Skip to content
Closed
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
26 changes: 26 additions & 0 deletions .github/workflows/ts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
40 changes: 36 additions & 4 deletions ontology/kko/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@
**Bytes:** 327,797
**KKO ontology version:** v2.00 (`owl:versionIRI <http://kbpedia.org/kbpedia/v200>`)
**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
Expand All @@ -26,7 +33,32 @@ KBpedia and the KKO are released under **CC-BY-4.0**.
Attribution required; see <https://kbpedia.org> and <https://creativecommons.org/licenses/by/4.0/>.
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.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
100 changes: 100 additions & 0 deletions scripts/check-kko-provenance.mjs
Original file line number Diff line number Diff line change
@@ -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}`)
Comment on lines +53 to +55

// ── 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]
Comment on lines +63 to +64
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.')
Comment on lines +63 to +67
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}
]
`
Comment on lines +77 to +92

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")
Comment on lines +94 to +97

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`)
Loading