Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:"
Expand All @@ -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"; \
Expand Down
Empty file added docs/.nojekyll
Empty file.
Binary file added docs/fonts/Canela-Regular.woff2
Binary file not shown.
Binary file added docs/fonts/IBMPlexMono-Regular.woff2
Binary file not shown.
Binary file added docs/fonts/InterVariable.woff2
Binary file not shown.
440 changes: 440 additions & 0 deletions docs/index.html

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/kp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
163 changes: 163 additions & 0 deletions scripts/build_docs.py
Original file line number Diff line number Diff line change
@@ -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:
* <meta name="..."> — citation_publication_date, citation_online_date,
citation_technical_report_number, citation_doi, DC.date, DC.identifier,
DC.rights
* <span data-kp="..."> 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 <meta> 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 <head> can't
# silently skip a field (a true miss still fails loudly via search()).
pat = re.compile(r'(<meta(?=[^>]*\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'(<span data-kp="' + re.escape(key) + r'"[^>]*>)([^<]*)(</span>)')
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())