From 058f87e73255cac57f26ede50fc1f8501aab2897 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:31:30 -0400 Subject: [PATCH] W12: make ontology/kko/PROVENANCE.md a gate instead of a record (#35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * W12: make ontology/kko/PROVENANCE.md a gate instead of a record The engine's KKO provenance is the best in the estate — source, licence, version IRI, byte count, sha256 — and until now it was verified by NOTHING. The digest was a correct line in a Markdown file, connected to no check. That matters beyond this repo: prophet-platform vendors byte-identical copies of this TBox and now asserts the SAME constant at import. The engine, the artifact's source of truth, was the one copy where drift would have gone unnoticed. scripts/check-kko-provenance.mjs (npm run check:kko, wired into ts-ci on every PR) asserts three things: 1. kko-2.10.n3 still hashes to the SHA-256 the record declares, and is still the declared byte length. The record is the source of the expectation, so record and artifact cannot silently disagree. 2. The record pins a COMMIT. It previously cited "@ master" — a moving reference: the same retrieval later can return different bytes while claiming this provenance. Now pinned to 3f888b397255b69d1439fd95823e97011ed9440b (verified against upstream 2026-07-29; master has not moved since 2019-04-09, which is exactly why the difference stays invisible until it isn't). 3. ts/src/kko-data.ts is exactly what scripts/gen-kko.mjs reproduces from that .n3 — so the ontology the engine SHIPS derives from the file whose provenance we publish. A hand-edit, or a re-vendor without regeneration, otherwise leaves the runtime ontology diverged from its stated source. Same doctrine as the existing ts/dist staleness gate. Read-only with respect to ts/src: it regenerates into memory and compares, never writes. Teeth verified — each check fails on a real tamper: 1. length-preserving :Monads -> :Places re-parent (valid Turtle, still 327,797 bytes) -> refused 2. commit pin reverted to "master" -> refused 3. hand-edited label in the generated kko-data.ts -> refused clean tree -> exit 0 Note: the commit-pin check deliberately strips the sha256 from the document before searching for a 40-hex commit — a naive scan matches a 40-char window INSIDE the 64-char digest and would have been satisfied by the very value it is meant to be independent of. * review: least-privilege token for ts-ci, and gate the paths the gate guards Two findings from review of #35. 1. CodeQL actions/missing-workflow-permissions fired on the new kko-provenance job (alert 36, ts-ci.yml:92). Accepted. Fixed at WORKFLOW level rather than on the new job, because the same rule was already open against this file on `main` for build-and-verify-dist (alert 33, ts-ci.yml:24) — a job-level block would have closed the alert this PR introduced and left the one it inherited. Declaring it once also means a job added later inherits least-privilege instead of silently getting the default write-scoped token. Both jobs only read the checkout and install from the public registry, so `contents: read` is sufficient; verified both still pass locally. 2. `on: push` was path-filtered to package.json / ts/** / the workflow itself. The kko-provenance job guards ontology/kko/** and scripts/**, so on a push that touched ONLY those paths the gate did not run at all. It always runs on pull_request (no path filter there), which is the merge path, so this was not a hole in practice — but the trigger should cover what the job guards. Added ontology/** and scripts/**. Gate-fires proof (all six failure branches, each restored afterwards): artifact digest drift -> rc=1 commit pin replaced by @master -> rc=1 kko-data.ts hand-edited -> rc=1 declared byte count wrong -> rc=1 declared sha256 removed -> rc=1 source repo no longer recorded -> rc=1 untouched tree -> rc=0 --- .github/workflows/ts-ci.yml | 26 ++++++++ ontology/kko/PROVENANCE.md | 40 +++++++++++-- package.json | 3 +- scripts/check-kko-provenance.mjs | 100 +++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 scripts/check-kko-provenance.mjs diff --git a/.github/workflows/ts-ci.yml b/.github/workflows/ts-ci.yml index 579b35f..585f0e8 100644 --- a/.github/workflows/ts-ci.yml +++ b/.github/workflows/ts-ci.yml @@ -16,9 +16,18 @@ on: paths: - "package.json" - "ts/**" + - "ontology/**" + - "scripts/**" - ".github/workflows/ts-ci.yml" pull_request: {} +# Both jobs only read the checkout and install from the public registry. Declared at workflow +# level so a job added later inherits least-privilege instead of silently getting the default +# write-scoped token (CodeQL actions/missing-workflow-permissions — which was already open +# against this file on `main` for build-and-verify-dist, not just the new job below). +permissions: + contents: read + jobs: build-and-verify-dist: runs-on: ubuntu-latest @@ -73,3 +82,20 @@ jobs: - name: No TS/dist changes — required check trivially satisfied if: steps.changed.outputs.ts != 'true' run: echo "This PR touches no package.json or ts/** files; build-and-verify-dist has nothing to verify." + + # ── Vendored-ontology provenance (W12) ───────────────────────────────────────────── + # ontology/kko/PROVENANCE.md recorded a sha256 that NOTHING verified. Consumers across the + # estate (prophet-platform apps/owl-reasoner) pin the same digest and assert it at import, so + # the engine — the artifact's source of truth — was the one copy where drift went unnoticed. + # Runs on every PR (cheap, no path filter) so a re-vendor can never land unverified. + kko-provenance: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install + run: npm install --no-audit --no-fund + - name: Verify vendored KKO provenance + generated kko-data.ts + run: npm run check:kko diff --git a/ontology/kko/PROVENANCE.md b/ontology/kko/PROVENANCE.md index 38d85ae..9279147 100644 --- a/ontology/kko/PROVENANCE.md +++ b/ontology/kko/PROVENANCE.md @@ -5,14 +5,21 @@ **Bytes:** 327,797 **KKO ontology version:** v2.00 (`owl:versionIRI `) **Namespace:** `http://kbpedia.org/ontologies/kko#` -**Retrieved:** 2026-07-19 +**Retrieved:** 2026-07-19 · **Provenance re-verified against upstream:** 2026-07-29 -## Source (sovereign) +## Source (sovereign, pinned) Vendored from the estate's sovereign fork, not upstream directly: -- **`SocioProphet/kbpedia`** @ `master`, path `versions/2.10/kko-demo.n3` - (raw: `https://raw.githubusercontent.com/SocioProphet/kbpedia/master/versions/2.10/kko-demo.n3`) +- **`SocioProphet/kbpedia`** @ commit `3f888b397255b69d1439fd95823e97011ed9440b` (branch `master`), + path `versions/2.10/kko-demo.n3` + (raw: `https://raw.githubusercontent.com/SocioProphet/kbpedia/3f888b397255b69d1439fd95823e97011ed9440b/versions/2.10/kko-demo.n3`) - Upstream: **`KBpedia/kbpedia`** (the org formerly known as Cognonto). +This record previously cited `@ master`. A branch name is a MOVING reference, not a pin: the same +retrieval run later can return different bytes while still claiming this provenance. `master` has +not moved since 2019-04-09, so the two resolve identically today — which is exactly why the +difference stays invisible until it isn't. `scripts/check-kko-provenance.mjs` now refuses a record +that pins no commit. + ## What it is The **KKO upper ontology** — the Peircean-grounded typology structure that types the KBpedia Knowledge Graph. This file carries the KKO TBox: **203 `owl:Class` declarations, 167 @@ -26,7 +33,32 @@ KBpedia and the KKO are released under **CC-BY-4.0**. Attribution required; see and . This vendored copy preserves that attribution and does not modify the ontology content. +## Where this record is ENFORCED +Until 2026-07-29 everything above was verified by **nothing** — the sha256 was a correct line in a +Markdown file, connected to no check. `scripts/check-kko-provenance.mjs` (npm run `check:kko`, run +in `ts-ci`) now asserts, on every PR: + +1. `kko-2.10.n3` still hashes to the **SHA-256** declared above, and is still the declared byte + length. The record is the source of the expectation, so record and artifact cannot silently + disagree. +2. This file pins a **commit**, not a branch. +3. `ts/src/kko-data.ts` is exactly what `scripts/gen-kko.mjs` reproduces from this `.n3` — so the + ontology the engine actually SHIPS derives from the vendored file whose provenance is published + here. (Same doctrine as the repo's `ts/dist` staleness gate: a generated artifact must be + reproducible from its source, or the source is not the source.) + +**This digest is load-bearing beyond this repo.** prophet-platform vendors byte-identical copies of +this TBox (`apps/owl-reasoner/src/owl_reasoner/data/kko-2.10.n3`) and pins the SAME constant, +asserted at import. Moving this digest without moving the consumers' is the drift these gates +exist to catch — change them in one PR each, never independently. + ## Regeneration The compact hierarchy consumed by the engine (`ts/src/kko-data.ts`) is generated from this file by `scripts/gen-kko.mjs` (parses with the engine's own `parseTurtle`). Re-run after re-vendoring a newer KKO version and commit both the `.n3` and the regenerated `kko-data.ts`. + +Re-vendoring checklist: +1. Copy the new `.n3` from the pinned `SocioProphet/kbpedia` path; update the commit pin above. +2. `shasum -a 256 ontology/kko/kko-2.10.n3` → update **SHA-256** and **Bytes** above. +3. `node --import tsx scripts/gen-kko.mjs` → commit the regenerated `ts/src/kko-data.ts`. +4. `npm run check:kko` must pass, then update the consumer pins listed above. diff --git a/package.json b/package.json index e50bcc5..de9183e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "prepare": "tsup", "typecheck": "tsc -p ts/tsconfig.json --noEmit", "test": "node --import tsx --test ts/src/*.test.ts", - "check:dist": "tsup && git diff --quiet -- ts/dist || (echo 'ERROR: ts/dist is stale vs ts/src — run: npm run build, then commit ts/dist' && exit 1)" + "check:dist": "tsup && git diff --quiet -- ts/dist || (echo 'ERROR: ts/dist is stale vs ts/src \u2014 run: npm run build, then commit ts/dist' && exit 1)", + "check:kko": "node --import tsx scripts/check-kko-provenance.mjs" }, "optionalDependencies": { "autobase": "7.28.1", diff --git a/scripts/check-kko-provenance.mjs b/scripts/check-kko-provenance.mjs new file mode 100644 index 0000000..a5098cd --- /dev/null +++ b/scripts/check-kko-provenance.mjs @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/** + * KKO vendored-ontology provenance guard. + * + * ── Why this exists (W12 inventory hygiene, 2026-07) ───────────────────────────────── + * `ontology/kko/PROVENANCE.md` is the best provenance record in the estate — source, licence, + * version IRI, byte count, sha256. It is also, until this script, verified by NOTHING. The + * digest was a line in a Markdown file: correct, and connected to no check. Two consumers + * (prophet-platform's owl-reasoner and nugget-extractor's KKO type refs) pin the SAME digest and + * now assert it at import, so the engine — the artifact's source of truth — was the one copy + * where drift would have gone unnoticed. + * + * ── What it enforces ───────────────────────────────────────────────────────────────── + * 1. `ontology/kko/kko-2.10.n3` still hashes to the sha256 recorded in PROVENANCE.md, and is + * still the recorded byte length. The doc is the source of the expectation, so the doc and + * the artifact can never silently disagree. + * 2. PROVENANCE.md pins a COMMIT, not just a branch. `@ master` is a moving reference: re-run + * the retrieval later and you may get different bytes while claiming the same provenance. + * 3. The generated `ts/src/kko-data.ts` is exactly what `scripts/gen-kko.mjs` produces from + * that .n3 — i.e. the ontology the engine actually SHIPS derives from the vendored source. + * A hand-edit to kko-data.ts, or a re-vendored .n3 without a regeneration, otherwise leaves + * the runtime ontology silently diverged from the file whose provenance we publish. + * (Same doctrine as this repo's existing ts/dist staleness gate — a generated artifact must + * be reproducible from its source, or the source is not the source.) + * + * Read-only: it regenerates into memory and compares. It never writes ts/src. + * + * Run: node --import tsx scripts/check-kko-provenance.mjs + */ +import { readFileSync } from 'node:fs' +import { createHash } from 'node:crypto' +import { fileURLToPath } from 'node:url' + +const die = (m) => { console.error(`✗ ${m}`); process.exit(1) } +const url = (p) => fileURLToPath(new URL(p, import.meta.url)) + +const n3Path = url('../ontology/kko/kko-2.10.n3') +const mdPath = url('../ontology/kko/PROVENANCE.md') +const dataPath = url('../ts/src/kko-data.ts') + +// ── 1) the artifact matches the digest ITS OWN provenance record declares ── +const doc = readFileSync(mdPath, 'utf8') +const declared = (doc.match(/\*\*SHA-256:\*\*\s*`([0-9a-f]{64})`/) || [])[1] +if (!declared) die('ontology/kko/PROVENANCE.md does not declare a **SHA-256:** `<64hex>` — the record must state what the artifact should be') + +const bytes = readFileSync(n3Path) +const actual = createHash('sha256').update(bytes).digest('hex') +if (actual !== declared) + die(`vendored KKO TBox DRIFTED: sha256 ${actual} != ${declared} declared in ontology/kko/PROVENANCE.md.\n` + + ` This digest is pinned by consumers too (prophet-platform apps/owl-reasoner asserts it at import).\n` + + ` Re-vendor, then update PROVENANCE.md AND every consumer pin in the same change.`) + +const declaredBytes = Number(((doc.match(/\*\*Bytes:\*\*\s*([\d,]+)/) || [])[1] || '0').replace(/,/g, '')) +if (declaredBytes && bytes.length !== declaredBytes) + die(`vendored KKO TBox is ${bytes.length} bytes, PROVENANCE.md declares ${declaredBytes}`) + +// ── 2) the source must be PINNED, not a moving branch ref ── +// NB: search for the commit pin only AFTER removing the sha256 from the text. A naive +// /[0-9a-f]{40}/ over the whole document matches a 40-char window INSIDE the 64-char sha256 — +// the check would have been satisfied by the very digest it is supposed to be independent of. +// (A checker that validates itself validates nothing; this repo's estate has been bitten by +// exactly that shape before.) +const withoutDigest = doc.split(declared).join('') +const commitPin = (withoutDigest.match(/\b([0-9a-f]{40})\b/) || [])[1] +if (!commitPin) + die('ontology/kko/PROVENANCE.md pins no commit sha. "@ master" is a MOVING reference — the same ' + + 'retrieval later can return different bytes and still claim this provenance. Pin the commit.') +for (const needle of ['SocioProphet/kbpedia', 'CC-BY-4.0']) + if (!doc.includes(needle)) die(`ontology/kko/PROVENANCE.md does not record ${needle}`) + +// ── 3) the SHIPPED ontology is reproducible from the vendored source ── +// Imported lazily and by URL so this file stays plain .mjs; needs `node --import tsx`. +const { parseKko } = await import('../ts/src/kko.ts') +const onto = parseKko(bytes.toString('utf8')) +onto.classes.sort((a, b) => a.iri.localeCompare(b.iri)) // same deterministic order gen-kko.mjs uses + +const rows = onto.classes + .map((c) => { + const label = c.label !== undefined ? `, label: ${JSON.stringify(c.label)}` : '' + return ` { iri: ${JSON.stringify(c.iri)}${label}, subClassOf: ${JSON.stringify(c.subClassOf)} },` + }) + .join('\n') + +const expected = `// GENERATED by scripts/gen-kko.mjs from ontology/kko/kko-2.10.n3 — do not edit by hand. +// KKO ${onto.version} · ${onto.classes.length} classes. Regenerate after re-vendoring KKO. +import type { KkoClass } from './kko' + +export const KKO_VERSION = ${JSON.stringify(onto.version)} +export const KKO_CLASSES: KkoClass[] = [ +${rows} +] +` + +if (readFileSync(dataPath, 'utf8') !== expected) + die('ts/src/kko-data.ts is NOT what scripts/gen-kko.mjs produces from ontology/kko/kko-2.10.n3.\n' + + ' The ontology the engine ships has diverged from the vendored file whose provenance we publish.\n' + + " Run: node --import tsx scripts/gen-kko.mjs then commit ts/src/kko-data.ts") + +console.log(`✓ KKO provenance verified — ${actual.slice(0, 16)}… (${bytes.length.toLocaleString()} bytes), ` + + `${onto.classes.length} classes ${onto.version}; ts/src/kko-data.ts reproduces from source`)