diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3ef17c..00c1f94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: run: | if grep -rinE \ --include='*.md' --include='*.txt' --include='*.yml' --include='*.yaml' \ - --include='*.json' --include='*.cff' --include='*.toml' \ + --include='*.json' --include='*.cff' --include='*.toml' --include='*.html' \ --exclude='lefthook.yml' --exclude='ci.yml' --exclude-dir='.git' \ 'cross-model (review|consultation|spec review)|multi-model (synthesis|analysis|consultation|spec review)|three-model consultation|two-model consultation|round [0-9]+ (review|verify|cross-model|findings)|consulted (codex|gemini|claude)|adversarial review|(codex|gemini|claude) feedback|reviewer [0-9]+\b|session handover|model attributions' \ .; then @@ -66,6 +66,13 @@ jobs: done - name: Check relative links run: python3 scripts/check_links.py + # The spec home page (docs/index.html) carries release metadata — + # version, date, DOIs — projected from CITATION.cff by + # scripts/build_docs.py. This gate fails if the page was not + # regenerated after a CITATION.cff change, so the published page can + # never show a stale version/date/DOI. + - name: Check docs metadata in sync with CITATION.cff + run: python3 scripts/build_docs.py --check - name: Run conformance suite run: python3 conformance/run.py - name: Run conformance suite (strict PEG) diff --git a/Makefile b/Makefile index 0bfd1dc..9d2ed3a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PYTHON ?= python3 PACK ?= -.PHONY: help install conformance compiler-test pack lint clean +.PHONY: help install conformance compiler-test pack lint clean docs docs-check help: @echo "KP:1 conformance targets" @@ -18,6 +18,8 @@ help: @echo " make compiler-test Run the experimental graph compiler unit tests" @echo " make pack PACK=path Validate a single pack at PATH" @echo " make lint Same as conformance (alias)" + @echo " make docs Sync docs/index.html metadata from CITATION.cff" + @echo " make docs-check Verify docs/index.html is in sync (CI / pre-commit)" @echo " make clean Remove __pycache__ and .pyc files" @echo "" @echo "Examples:" @@ -35,6 +37,12 @@ compiler-test: lint: conformance +docs: + $(PYTHON) scripts/build_docs.py + +docs-check: + $(PYTHON) scripts/build_docs.py --check + pack: @if [ -z "$(PACK)" ]; then \ echo "error: PACK is required. Usage: make pack PACK=path/to/your-pack.kpack"; \ diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/fonts/Canela-Regular.woff2 b/docs/fonts/Canela-Regular.woff2 new file mode 100644 index 0000000..496017f Binary files /dev/null and b/docs/fonts/Canela-Regular.woff2 differ diff --git a/docs/fonts/IBMPlexMono-Regular.woff2 b/docs/fonts/IBMPlexMono-Regular.woff2 new file mode 100644 index 0000000..4297ee3 Binary files /dev/null and b/docs/fonts/IBMPlexMono-Regular.woff2 differ diff --git a/docs/fonts/InterVariable.woff2 b/docs/fonts/InterVariable.woff2 new file mode 100644 index 0000000..970a183 Binary files /dev/null and b/docs/fonts/InterVariable.woff2 differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..49b9945 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,440 @@ + + + + + +KP:1 — Knowledge Pack Format Specification + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+

Editor's Draft · Public Draft 2026-06

+

KP:1

+ +

An open, text-native format for packaging epistemic state — claims, confidence, evidence, contradictions, and the trace of how beliefs evolved, preserved as structure.

+ +

+ v0.8.4-preview + 2026-06-13 + CC BY 4.0 + DOI 10.5281/zenodo.19445262 +

+
+
+ + +
+
+

Abstract

+
+

KP:1 is a text-native specification for representing epistemic state — claims with confidence, evidence, contradictions, and the trace of how beliefs evolved — designed for AI inspectability and portability across systems. The format defines a strict normative core (CORE.md, JSON Schema, PEG grammar) plus topic-authoritative companions for voice surfaces, multilingual support, archival, composition, and extension blocks.

+
+
+
+ + +
+
+

What makes KP:1 distinct

+

Most formats flatten knowledge into settled facts. KP:1 keeps its shape: the uncertainty, the tension, and the history stay as addressable structure. The clearest way to see that is the format itself.

+ + +
+
+ claims.md — acquisition review + + 3 claims + self-describing + +
+
<!-- KP:1 — self-describing header: a reader bootstraps the format from the file
+     - [ID] assertion  {confidence|type|evidence|date|depth|nature}   (depth, nature optional)
+     type   o observed · r reported · c computed · i inferred
+     rel    → supports · ⊗ contradicts · ⊗! error · ⊗~ tension · ⊘ supersedes -->
+
+- [C005] Dating follows the catalogue raisonné: 1962, not 1957.
+  {0.93|r|E003|2020-04-30|exhaustive} ⊗!C004
+
+- [C014] The 2025 appraisal values the work at €950,000.
+  {0.91|r|E008|2025-12-15|investigated} ⊘C013
+
+- [C017] The 1953–1962 provenance gap is a material risk.
+  {0.45|i|E005,E010|2026-05-09|investigated|judgment} ⊗~C019
+
+ C017 ⊗~ C019 + A judgment held at 0.45, on two cited sources, kept in productive tension with the recommendation, not rounded away. Every line stays addressable, and the header lets any reader read the notation without fetching a schema. +
+
+ + +
    +
  • + +
    +

    Typed contradiction qualifiers

    +

    Disagreement is kept in tension: plain contradiction, known error, or productive tension. Nothing is flattened away.

    +
    +
  • +
  • + +
    +

    Voyager-Principle Rosetta header

    +

    Zero-dependency parsing: a reader can bootstrap the format from the file itself, with no external schema to fetch.

    +
    +
  • +
  • + +
    +

    Supersession with append-only history

    +

    When a belief changes, the prior claim is superseded, not deleted, so how the thinking moved stays legible.

    +
    +
  • +
  • + +
    +

    Three-surface architecture

    +

    One source of truth, three reads: claims for AI reasoning, display views for visual reading, voice views for spoken delivery.

    +
    +
  • +
+
+
+ + +
+ +
+ + + + +
+ + diff --git a/docs/kp.svg b/docs/kp.svg new file mode 100644 index 0000000..ac6ef42 --- /dev/null +++ b/docs/kp.svg @@ -0,0 +1,11 @@ + + KP:1 + + + + + + + + + diff --git a/docs/og-image.png b/docs/og-image.png new file mode 100644 index 0000000..e5749cc Binary files /dev/null and b/docs/og-image.png differ diff --git a/lefthook.yml b/lefthook.yml index 987c8db..9d5f4af 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -41,6 +41,16 @@ pre-commit: fi done + # Generated-file guard: docs/index.html carries version/date/DOIs + # projected from CITATION.cff by scripts/build_docs.py. Block a commit + # that would publish a page out of sync with the citation record. + # Prefers the repo venv; falls back to python3 (needs PyYAML, which + # `make install` provides). + docs-sync: + run: | + if [ -x .venv/bin/python ]; then PY=.venv/bin/python; else PY=python3; fi + "$PY" scripts/build_docs.py --check + # Public repo: scan staged content for AI-consultation process language. # This is a backstop — the primary defense is agent awareness in the # containing workspace. Workspace-specific codename and path patterns @@ -50,7 +60,7 @@ pre-commit: # keep the two pattern sets in sync. ci.yml is excluded below for the # same reason this file is: it carries the patterns themselves. deny-list: - glob: "*.{md,txt,yml,yaml,json,cff,toml}" + glob: "*.{md,txt,yml,yaml,json,cff,toml,html}" exclude: - "lefthook.yml" - ".github/workflows/ci.yml" @@ -78,7 +88,7 @@ pre-commit: # local patterns file), this check is a no-op and the only defense # is the behavioral rule in AGENTS.md §"What you MUST NOT do". client-names: - glob: "*.{md,txt,yml,yaml,json,cff,toml}" + glob: "*.{md,txt,yml,yaml,json,cff,toml,html}" # Array-of-globs form: a bare string here is a glob, not a regex, # so pipe-delimited lists silently exclude nothing. exclude: diff --git a/scripts/build_docs.py b/scripts/build_docs.py new file mode 100755 index 0000000..b922996 --- /dev/null +++ b/scripts/build_docs.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: 2026 Timothy Kompanchenko +"""Project release metadata from CITATION.cff into docs/index.html. + +The spec home page (docs/index.html) shows the current version, release date, +licence, and DOIs. Those are release-cadence values: hand-editing them means +the page is wrong the moment a new version is cut. This script makes +CITATION.cff the single source of truth and writes those values into the page, +so they cannot drift. + +Usage: + python3 scripts/build_docs.py # inject values into docs/index.html + python3 scripts/build_docs.py --check # exit 1 if the page is out of sync + make docs / make docs-check # the same, via the Makefile + +It modifies nothing else in the page. The fields it owns: + * — citation_publication_date, citation_online_date, + citation_technical_report_number, citation_doi, DC.date, DC.identifier, + DC.rights + * anchors in the body — version, date, license, + concept-doi, version-doi, year, ym +""" +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parent.parent +CFF = ROOT / "CITATION.cff" +PAGE = ROOT / "docs" / "index.html" + + +def fail(msg: str): + sys.stderr.write(f"build_docs: error: {msg}\n") + raise SystemExit(2) + + +def load_fields() -> dict: + """Read the canonical release metadata out of CITATION.cff.""" + data = yaml.safe_load(CFF.read_text(encoding="utf-8")) + if not isinstance(data, dict): + fail("CITATION.cff did not parse as a mapping") + + version = str(data.get("version") or "").strip() + released = str(data.get("date-released") or "").strip() + license_id = str(data.get("license") or "").strip() + if not (version and released and license_id): + fail("CITATION.cff is missing version, date-released, or license") + + m = re.fullmatch(r"(\d{4})-(\d{2})-(\d{2})", released) + if not m: + fail(f"date-released is not YYYY-MM-DD: {released!r}") + year, month, day = m.groups() + + dois = [i for i in (data.get("identifiers") or []) if i.get("type") == "doi"] + # The concept (evergreen) DOI describes itself as spanning all versions; the + # per-version DOIs only mention "concept" to point back at it. Match the + # evergreen wording, not the bare word "concept", so detection is unambiguous. + concept_hits = [i["value"] for i in dois + if any(k in str(i.get("description", "")).lower() + for k in ("evergreen", "across all versions", "always resolves"))] + version_hits = [i["value"] for i in dois if version in str(i.get("description", ""))] + if len(concept_hits) != 1: + fail("expected exactly one concept (evergreen) DOI: an identifier whose description says " + f"'evergreen' / 'across all versions' / 'always resolves'; found {len(concept_hits)}") + if len(version_hits) != 1: + fail(f"expected exactly one DOI whose description names {version!r}; found {len(version_hits)}") + concept = concept_hits[0] + version_doi = version_hits[0] + + return { + # body data-kp anchors + "version": f"v{version}", + "date": released, + "license": license_id.replace("-", " "), # display form: CC-BY-4.0 -> CC BY 4.0 + "_license_spdx": license_id, # SPDX form for machine metadata (DC.rights) + "concept-doi": concept, + "version-doi": version_doi, + "year": year, + "ym": f"{year}-{month}", + # head values + "_date_slash": f"{year}/{month}/{day}", + "_concept_url": f"https://doi.org/{concept}", + } + + +def apply(html: str, f: dict): + """Return (new_html, list_of_changes).""" + changes: list[str] = [] + + def meta(name: str, value: str, html: str) -> str: + # Tolerate attribute order/spacing so an IDE reformat of can't + # silently skip a field (a true miss still fails loudly via search()). + pat = re.compile(r'(]*\bname="' + re.escape(name) + r'")[^>]*?\bcontent=")([^"]*)(")') + mt = pat.search(html) + if not mt: + fail(f"meta tag not found: {name}") + if mt.group(2) != value: + changes.append(f"meta {name}: {mt.group(2)!r} -> {value!r}") + return pat.sub(lambda m: m.group(1) + value + m.group(3), html, count=1) + + def anchor(key: str, value: str, html: str) -> str: + pat = re.compile(r'(]*>)([^<]*)()') + if not pat.search(html): + fail(f"data-kp anchor not found: {key}") + + def repl(m): + if m.group(2) != value: + changes.append(f"data-kp {key}: {m.group(2)!r} -> {value!r}") + return m.group(1) + value + m.group(3) + + return pat.sub(repl, html) + + html = meta("citation_publication_date", f["_date_slash"], html) + html = meta("citation_online_date", f["_date_slash"], html) + html = meta("citation_technical_report_number", f["version"], html) + html = meta("citation_doi", f["version-doi"], html) + html = meta("DC.date", f["date"], html) + html = meta("DC.identifier", f["_concept_url"], html) + html = meta("DC.rights", f["_license_spdx"], html) + for key in ("version", "date", "license", "concept-doi", "version-doi", "year", "ym"): + html = anchor(key, f[key], html) + + return html, changes + + +def main() -> int: + ap = argparse.ArgumentParser(description="Sync docs/index.html metadata from CITATION.cff") + ap.add_argument("--check", action="store_true", + help="exit 1 if the page is out of sync (writes nothing)") + args = ap.parse_args() + + f = load_fields() + original = PAGE.read_text(encoding="utf-8") + updated, changes = apply(original, f) + + if args.check: + if changes: + sys.stderr.write("docs/index.html is OUT OF SYNC with CITATION.cff:\n") + for c in changes: + sys.stderr.write(f" - {c}\n") + sys.stderr.write("Run `make docs` and commit the result.\n") + return 1 + print(f"In sync: version {f['version']}, released {f['date']}.") + return 0 + + if updated != original: + PAGE.write_text(updated, encoding="utf-8") + print(f"Updated docs/index.html ({len(changes)} field(s)):") + for c in changes: + print(f" - {c}") + else: + print(f"docs/index.html already current (version {f['version']}, released {f['date']}).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())