v3.11: kernel creation works, every function resolves its own kernel, and events name who produced them — 0.4.36→0.4.49 - #62
Merged
Conversation
added 20 commits
August 10, 2026 17:20
…es inShape as a schema, and MEASURES the registry split — B1, v0.4.36
`affordances` answered from an unfiltered SPARQL scan for `?a a ckp:Affordance`
across every graph — no kernel filter, no retirement check, no inShape, and
reading a graph nobody writes, while dispatch resolved from the relational
ckp.affordance_registry and the governed truth sat in sealed ckp.instances.
THREE stores, not two. `[]` was the honest answer for a substrate holding
sealed affordances, and it reads exactly like "no grants".
ckp.affordances_of(project) now derives from sealed ckp:Affordance instances:
kernel-filtered on the substrate-stamped producedBy (unforgeable), retirement
honoured against the live epoch, and each carrying plane, delegate, derivedBy
and sealedAtEpoch — every field the root's AffordanceShape already gates, so
the projection invents nothing.
inShape becomes a CONTRACT, not a name. ckp._affordance_schema resolves it out
of the composed surface into paths + datatype + cardinality + nodeKind +
pattern. Measured through the door: adoption.declare returns intoProject
(IRI, required), sourceDigest (string, required, ^[0-9a-f]{64}$), intoEpoch
(integer, required), plus the optionals. That is what lets a conformant TOOL
organ hold no schema of its own — it reads its contract from the kernel
definition, and an applied change advances it with zero tool-code edits.
G-1 honestly: body locality forbids the gate requiring inShape to resolve, so
the projection REPORTS rather than asserts — an unresolvable shape yields
schema:null with schema_resolved:false. Controlled: a dangling inShape seals
(as the gate permits) and projects as absent, never as an invented contract.
DRIFT MEASURED, NOT MERGED. Registry verbs with no sealed Affordance return
under `unsealed`. First reading on the bench: **1 declared, 21 unsealed** —
the substrate's own dispatch surface is almost entirely undeclared. Unioning
the stores would have hidden exactly the hand-registered action derivedBy's
comment claims cannot hide. Closing the split by construction is #56 proper;
this makes it countable first.
Part of #56.
…epoch sealed — B4 (pgCK's half), v0.4.37
urn:ckp:pgCK/kernel/ck was wiped to zero triples by a concurrent writer on
2026-08-10. Nothing raised: _composed_shapes rebuilt the surface WITHOUT the
kernel's shapes — silently narrower, still "conforming" — and it was found only
because a query returned 0 rows by luck.
Preventing the wipe is graph lifecycle, which lives in the engine layer (pgRDF
0.6.25 refuses to validate an `untracked` graph, and pgCK's graphs are all
untracked; the lock primitive is not in pgrdf's SQL surface). So prevention is a
coordination ask, filed, not faked here. DETECTION is entirely pgCK's, and
detection is what was missing.
The tripwire already existed and 0.4.31 never read it back: every governed apply
seals a Materialization carrying sourceDigest (the kernel graph) and
surfaceDigest (the composed surface), and an Epoch carrying the same. The ledger
has held a signed statement of what the surface was, all along.
ckp.surface_check compares the live digests against it — P12/I14 made
operational: the surface in force is established by digest, not assumed to still
be what it was.
FIRST RUN FOUND WHAT WAS ALREADY SEALED. sourceDigest at epoch 2 was
e3b0c44298fc… — the sha256 of the EMPTY STRING. The kernel graph was already
wiped when the wave's first governed apply ran; the Materialization truthfully
sealed "the governed source shapes were nothing", the epoch advanced, and nobody
read it. Remediated the governed way: a new apply reseals at epoch 3 (surface
and source both match, findings empty). Epoch 2 stands on the record with its
honest empty-source digest — fence, never backfill (S5).
Reports, never refuses — deliberately. A false positive would take the substrate
down, and legitimate drift exists (adoption changes the surface without advancing
the epoch). A gate that can be wrong about the surface is worse than a report
that is always right about what it measured.
Two structural findings recorded en route: the door refuses any verb the registry
does not carry (unknown_affordance) — so a CASE branch alone is not a verb; and
the registry's `plane` column conflates ROUTING mechanism ('query'/'derived' mean
a compiled plan, 'instance' means the built-in CASE) with the root's semantic
plane. Both #56-adjacent.
Part of #56.
…— B3 + B1a, v0.4.38 B3 — ckp.integrity_check. Body locality means the instance gate reads the candidate plus its stamped parent closure and nothing a referenced node carries, so cross-node integrity is not checkable at the gate BY DESIGN and the root declares it as a gap (§4.5). This is the conformant path: a store-level check over sealed rows. It closes pgCK#59 point 2 — made possible by 0.4.33 persisting the stamps, made real here. Measured: unattributed 0 across 23 sealed rows. Catches what the gate cannot: a dangling derivedBy (proven sealable on the bench), an inShape that resolves to nothing, an Adoption whose Module is not on the ledger, a Grant targeting a non-Organ (§4.5's own worked example, zero rows today — the check exists so the first wrong one is seen rather than discovered). TWO DISCIPLINES BUILT INTO THE CHECK ITSELF, both learned today: * Scars are not defects. Rows sealed at epoch 0 predate the root being in force, cannot be fixed (nothing is ever unsealed), and are legitimate history under S5 — fence, never backfill. They report under `historical`, not `findings`, because a check that can never go green trains its reader to ignore it, which is exactly how a real defect gets missed. Vacuity at epoch >= 1 IS a finding. * Withdrawal is sealed, not deleted. A superseded Affordance is out of force and must stop raising, so the G-1 loops honour ckp:Supersession the way _adopted_graphs already does. Demonstrated end to end: the deliberate dangling control was withdrawn by a sealed Supersession carrying its reason, and the audit went healthy — without deleting a row or editing history. B1a — ckp.authority_of. Persona spec §3 SHOULD: authority is a traversal over sealed edges, computed at need, never cached. Surfaced by CK.Lib.Js's I5 — a client discovers its grants by violation, having no way to ask. The point is that an EMPTY chain reports as empty with a reason: "not the same as unchecked". Anonymous is answered as a TIER, not an identity (PR1: nothing durable accretes to it), and it says plainly that transport grants are minted at admission and are not readable here. Both are READS. Neither gates. Part of #56, #59.
… at the import door — C, v0.4.39 MEASURED FIRST, AND IT RESIZED THE PROBLEM. The loaded urn:ckp:core is byte-identical to the published root (e5f7d1e5…), zero instances and zero quads carry a v3.7 predicate, and no board graph exists. And the operator's point, verified rather than accepted: NOTHING v3.8 CAN REACH THE GATE BY THE LOCKED DEFINITION. Since 0.4.35 the enforcement surface has exactly three inlets — the epsilon-0 core, <project>/kernel/ck, and the graph of every sealed unsuperseded Adoption. import_module writes to <project>/kernel/board, which is none of them, and no Adoption names a v3.8 module (0). So this change is HYGIENE AND DEFENCE IN DEPTH, not the closure of an open hole. Saying otherwise would be the decorative-protection failure the threat table names, so the header says what it is. Retired outright (nothing to migrate — their terms are in the root or were cut by ruling): affordance, delegation, proof declare ckp:Affordance/Delegation/Proof at v3.8, which the v3.11 root declares itself; delivery and validate declare ckp:Delivery/ckp:Validation, cut by §8.2. They were dead weight and a trap for anyone grepping v3.8 to find live use. task and goal are NOT deleted and their destination is NOT decided here — moving them changes the board verbs a consumer calls, which is not a call to make inside a namespace fix. Instead R7 becomes structural at the door: import_module refuses any module whose file declares a term in the core namespace, naming the rule and the offending terms, checked on the file text before a triple is parsed. The violation cannot ship while the destination is still open. Their defect is real even though it cannot reach the gate: a board graph carrying declarations the published root does not make is a false core to anything reading it. Part of #46, #47.
…ed by project demo
The relay hardcoded the pgCK segment and the bgworker called the two-argument
ckp.dispatch(verb, payload), which has no kernel-URN. Every project parameter
then fell to DEFAULT 'demo', so a write addressed to input.kernel.pgCK.action.*
was judged by urn:ckp:demo/shapes/composed — ten v3.8 NodeShapes that declare
nothing about a v3.11 body.
Measured: a wave:Index carrying role "banana" and 1 of 8 required properties
SEALED CLEAN, producedBy urn:ckp:demo/kernel/ck, conformsToShape absent. The
gate was never vacuous — it was never asked.
The SQL side needed no change: ckp.dispatch already read
COALESCE(current_setting('ckp.project', true), 'demo') and was simply never
told. So the kernel is now carried the same way identity already was, as a
transaction-local GUC beside ckp.requester.
nats_client.rs RELAY_IN_SUBJECT/RELAY_ID_SUBJECT take a wildcard kernel
segment; route_inbound parses it as ONE token instead of
stripping a literal; result_subject follows the caller
inbound_dispatch.rs InboundAction carries `kernel`; dispatch_one sets
ckp.project transaction-locally
Subscription is transport; the callout grant is policy. Hardcoding the segment
made the relay a second policy point — the callout minted grants for kernels it
never subscribed, so Dictionary held a grant with no listener and CK-dev,
pgCK.MCP and CK.Lib.Js were unreachable.
GATE (re-run, passes): the identical body is now refused with every clause
named — 7x MinCountConstraintComponent and 1x InConstraintComponent on role.
Two tests pin it: a non-pgCK kernel must route carrying its own segment, and
the segment must stay exactly one token so a dotted name cannot span segments.
Confirms CK-dev's independently filed hypothesis. Their proposed remedy
(registering ckp.affordance_registry rows) addressed a consequence, not a cause.
Refs PASS-27.
… then remove from disk — 0.4.40
ontology/ now holds README.md and v3.11/ and nothing else. The order matters:
deleting first yields pg_read_file failures and a container-start hazard;
deleting last yields a reason.
Measured against the loaded root (e5f7d1e5) before writing a line: ckp:Goal and
ckp:Task DO NOT EXIST in v3.11 — querying the core for rdfs:label over
{Goal, Task, Kernel, Instance, Organ} returns three rows. The board vocabulary
is the adopted wave module. So the pair is RETIRED, not relocated.
ckp.boot default -> /ontology/v3.11/core.ttl. The only hard file
dependency in the install path; its pg_read_file is
unwrapped and raises.
ckp.import_module known set EMPTY by ruling; retired names refuse with a
reason naming the successor, because "unknown module" is
the wrong answer for a thing deliberately withdrawn.
ckp.load_kernel board imports removed. They were exception-wrapped, so the
failure was quiet rather than absent — every kernel load
warned about modules that no longer exist.
pgck-baseline.sql boot repointed HERE TOO. The baseline is the fresh-install
path, so fixing only the migration would have left
CREATE EXTENSION booting at a file this commit deletes.
Two-route parity, the trap PASS-25 recorded once already.
compose.yml the /ontology base was fixtures/ontologies — a v3.7 corpus
(core, base-instances, consensus, edges, kernel-metadata)
with three per-file overlays papering over it. All four
collapse to one mount of ontology/.
Digests verified against the CK-org authority, not against memory:
core e5f7d1e5 == CK-org == sidecar pass-10
wave f4ad27ce == CK-org == sidecar pass-12
lexicon ce9f20f4 == CK-org == sidecar pass-19
The pass-7 sidecar (7d838610) does not match and must not: it is the historical
digest PASS-7 measured D1 against. Sidecars are version-stamped by filename.
PROVEN on the bench, v3.11-only, nothing lost:
NOTICE: ckp.boot: core graph loaded from /ontology/v3.11/core.ttl, 27 NodeShapes
core triples 1225 (unchanged) · sealed rows 28 (unchanged)
import_module('task'|'goal'|'proof') -> RETIRED, not missing
ck_create wave:Index {"role":"banana"} -> refused, 8 clauses named
Removed: ontology/{core,core.sha256,task,goal}.ttl, ontology/v3.8/,
examples/goal-task-board.kernel.ttl (v3.7+v3.11 mixed, unreferenced).
example.kernel.ttl stays — it is v3.11 and both smoke gates load it.
KNOWN OPEN, not hidden: smoke-s4 fails at s16, which calls ckp.shapes_self_test
— a PL/pgSQL function enumerating ('ckp:TaskShape','ckp:Task') by hand. That is
a second enforcement surface, the pattern SPEC.CKP v3.11 4.4 already forbids,
and it was invisible only while the names it hardcoded happened to exist. It is
to be retired, not updated; non-vacuity is already served generically by
ckp.surface_check against the digest its epoch sealed.
Refs PASS-28.
…rd assertions — 0.4.40
shapes_self_test enumerated ('ckp:TaskShape','ckp:Task') and ('ckp:GoalShape',
'ckp:Goal') in a hand-written array and RAISEd when they were absent. That is a
procedural lookup outside the validator — the pattern SPEC.CKP.v3.11 §4.4
already forbids for the admitted-type set, one level over. It was invisible
only while the names it hardcoded happened to exist; retiring the v3.7 board
made it unsatisfiable, which is how it was found. Updating the list to
wave:TicketShape would relocate the defect, not remove it.
What was legitimate inside it survives, because a SHACL validator genuinely
cannot report that it validated against NOTHING — zero targeting shapes returns
conforms:true, indistinguishable from a real pass. So the check becomes exactly
that property, name-free, and NON-MUTATING (it reads the composed graph or falls
back to core; it does not call _composed_shapes, which TRUNCATEs and rebuilds).
TWO-ROUTE PARITY, four times in one night. Each time the upgrade route was
correct and the install route lagged:
ckp.boot baseline still defaulted to /ontology/core.ttl
shapes_self_test fixed in the migration, stale in the baseline
shapes_self_test AGAIN redefined in pgck--0.4.1--0.4.2.sql, the
pgck_install_completeness file, which src/lib.rs
guarantees runs LAST — so a fresh CREATE EXTENSION
silently overwrote the corrected body
import_module/load_kernel migration-only; fresh install still read the files
Install and upgrade now agree on all four.
s34's board assertions are INVERTED rather than deleted. The gate used to assert
"boot + import_module from shipped /ontology works" and "the full legacy board
verb still works". Both tested a world that is now retired, so they now assert
the refusal — and specifically that it names the RETIREMENT rather than a
missing file, because a refusal is a result and "could not open file" is the
wrong answer for a deliberate withdrawal.
GATES:
smoke-s4 GREEN — all 54 files, including the role floor (s11/s13/s14 with
pgrdf.sparql, materialize and SELECT ckp.instances all denied to
ck_participant), identity persistence (s58/s60) and the affordance
/epoch path (s17/s41/s52/s55/s62)
smoke-s34 6 of 7 — virgin cluster, CREATE EXTENSION, tables out-of-the-box,
pre-boot write fail-closed, boot on /ontology/v3.11/core.ttl with
27 NodeShapes, retired module refusing WITH A REASON, governed
dispatch as a real ck_participant login ok:true
KNOWN BREACH, caught by the inverted assertion and NOT introduced here:
ckp.seal does not enforce the admitted type set. _type_admitted (:716) is called
from two places and NEITHER is seal, so an undeclared type reaching seal gets a
vacuous SHACL pass — no shape targets it, zero focus nodes, conforms:true — and
seals. task.create mints …/v3.7/Task while the board module declared TaskShape
targeting v3.11 ckp:Task; those never matched, so the board flow has been
sealing ungated instances all along. Retiring the module revealed it rather than
causing it. Fix follows in the next commit.
Refs PASS-28.
… exposed
BOTH GATES GREEN: smoke-s4 (54 files) and smoke-s34_fresh_install. First time
either has run against 0.4.33+ — PASS-PROV standing constraint 4, unmet since
0.4.33, is now met.
s34's keystone sealed {"task":{…}}, which mints …/ontology/v3.7/Task. It passed
only because ckp._type_admitted carries the #46 TRANSITIONAL ALLOWANCE that
waves every v3.7 type through. It now seals ckp:Supersession instead —
SupersessionShape requires exactly ckp:supersedes (IRI, minCount 1) — so the
keystone exercises the same chain (participant login -> dispatch ->
admitted-type -> SHACL gate -> seal) against vocabulary the root actually
declares, and no longer depends on the allowance to pass.
R2 IS OPEN ON THE WRITE PATH. Measured, not inferred:
ckp.seal DOES consult _type_admitted (baseline:3412) — an earlier reading of
mine said it did not, and that was a non-greedy regex stopping at the OPENING
$function$ delimiter rather than the closing one. Corrected.
The hole is the allowance itself. Deleting it was measured to work:
ckp.seal: type https://conceptkernel.org/ontology/v3.7/Task is not admitted
— no shape targets it and it is declared by no class in the composed surface
(undeclared types cannot seal; SHACL would validate them vacuously)
and smoke-s34 then passed with the board verb correctly refused.
It is NOT deleted here, because deleting it invalidates 30 board-verb call
sites across 8 tests (s15 5, s19 9, s37 1, s38 3, s45 3, s57 1, s58 6, s60 2)
where the board verb is the WRITE VEHICLE for something else entirely —
s58 tests server-derived identity, s60 tests msg.by. Migrating those is a
ticket, not a tail-end fix.
And the allowance's own exit condition is not met: "tolerate it until #46
re-points the body construction". #46 cannot re-point, because ckp:Task and
ckp:Goal do not exist in v3.11. So #46's real content is REMOVE the board
verbs and give those 30 call sites a v3.11 write vehicle — the same shape as
this commit's keystone change, thirty times.
Recorded inline at the s34 assertion that will flip when it lands, so the
next reader learns why it passes rather than assuming it is gated.
Pre-existing, not introduced by the ontology act: task.create minted v3.7 Task
while the board module declared TaskShape targeting v3.11 ckp:Task. Those never
matched. Retiring the module revealed the vacuity rather than causing it.
Refs PASS-28.
…ch_safe, 0.4.41
MEASURED 2026-08-11 22:35:47 on pgck.localhost:
ckp.seal: payload fails the composed shape gate … NodeKindConstraintComponent
background worker "pgck-bridge" (PID 113) exited with exit code 1
The worker came up healthy at 21:42, served all evening, and died on the first
seal REFUSAL. With it dead nothing answers $SYS.REQ.USER.AUTH, so every
subsequent CONNECT was rejected 'Authorization Violation' — the whole door closed
because the gate did its job. Both the MCP server and the CLI failed identically
and a valid bearer made no difference, which is what made it look like an auth
problem for most of an hour.
MECHANISM: ckp.seal RAISEs. Inside the bgworker's SPI call that Postgres ERROR
unwinds as a pgrx PANIC rather than a Result::Err, so inbound_dispatch's
`match out { Err(e) => …ok:false… }` never runs and the worker process
terminates. A refusal is a NORMAL, EXPECTED event — R2 and the shape gate exist
to produce them — so it must be data, never a fault of the transport.
FIX: ckp._dispatch_safe wraps ckp.dispatch. The catch belongs in SQL, not Rust,
because a PL/pgSQL EXCEPTION block opens a subtransaction — the refusal rolls
back cleanly on its own and the caller's transaction survives. It returns
{ok:false, refused:true, verb, sqlstate, error} and never re-raises; re-raising
is what killed the worker.
This ALSO closes PASS-27 Filing 2. The refusal now REACHES the caller instead of
being lost and presenting as a 60 s timeout — verified with the identical body:
{"ok": false, "refused": true, "sqlstate": "P0001",
"verb": "kernel.propose_change",
"error": "ckp.seal: payload fails the composed shape gate … NodeKind
constraint not satisfied. Expected node kind: Iri"}
worker-exit lines after the probe: 0
responder restarts after the probe: 0
Carried into BOTH routes at the same time — install and upgrade lagged each other
four separate times earlier in this pass, so parity is now done in one act rather
than discovered by a gate.
Spec claims proven live along the way, none of them by assertion:
§4.1 four of the twelve measured-enforcing components observed refusing:
minCount, in, pattern, nodeKind
§9 the Separation Axiom is a GATE: a ckp:CK organ claiming
writeAuthority "readwrite" was refused, 0 rows sealed, count unchanged
§7 version() and build_id() are different planes: 0.4.41 vs
v0.4.24-84-gedee737-dirty
Refs PASS-28, PASS-27 Filing 2.
fmt was the failing job on PR #62, and both diffs are code I inserted by hand — the _dispatch_safe comment block inside client.update(), and the route_inbound format! in the CK-dev regression test. Exactly the mistake PASS-PROV already records for this repo: "fmt — was the ONE job green on main; I broke it with hand-formatted test sites. Repaired via the pinned 1.97.1 in the shared builder (not the host formatter)." Repaired the same way, for the same reason: rust-toolchain.toml pins channel 1.97.1 precisely so a floating host toolchain cannot resolve differently per machine. Whitespace and comment indentation only — no semantic change. src/inbound_dispatch.rs comment block re-indented inside the call src/nats_client.rs format! wrapped across lines Verified `cargo fmt --all -- --check` CLEAN in the builder before pushing.
… and gains its shapes, 0.4.42 R2 IS NOW CLOSED ON THE WRITE PATH. The #46 transitional allowance is deleted and its own exit condition is met: "tolerate it until #46 re-points the body construction, at which point the board path becomes non-vacuously validated." Both halves land here, together. WHY TOGETHER. Moving one without the other is what failed twice: 1fb173e the SHAPE was re-pointed and the emission stayed — "a v3.8 shape targets nothing the re-pointed substrate emits, the PASS-10 vacuous direction" 2026-08-11 the DECLARATION was removed and the emission stayed (mine, PASS-28) Both produce a vacuous surface from opposite sides. Emission and shape move in one act — the PASS-18 lesson this repo had recorded twice and violated twice. WHY urn:ckp:board/ AND NOT wave:. wave:TicketShape requires openedAtPass and benchClass. Those are pass-process metadata; a consumer's board task has no pass and no bench class, so satisfying them means inventing values to please a shape — the "invented contract" this epoch exists to end. The board is DOMAIN vocabulary per SPEC.CKP.v3.11 §2, the same form s48 already proves with urn:ckp:consensus/. This is C1, which PASS-PROV left explicitly as an operator call; the evidence that decided it is TicketShape's two required fields. WHY FLAT rather than s48's type/prop split. ckp.dispatch carries ONE namespace constant N for both types (N||'Task') and properties (N||'title'), so a flat namespace repoints reads and writes coherently in a single change. A split needs the emitter restructured, and restructuring the emitter is where drift enters. WHY PROJECT-INDEPENDENT. ckp.load_kernel parses the kernel TTL into urn:ckp:<project>/kernel/ck for whatever project it is given, so a per-project IRI would make shape and emission drift apart per project. examples/example.kernel.ttl urn:ckp:board/{Task,Goal,Edge,Message} + 4 shapes, every minCount matching a field dispatch ALWAYS emits — more would refuse conformant work, less would be decoration N (5 declarations) …/ontology/v3.7/ -> urn:ckp:board/ 10 hardcoded v3.7 IRIs swept (they bypassed N, which is why the constant alone did not move them) _type_admitted allowance DELETED 64 test-fixture literals swept across 9 files RESIDUE: 15 v3.7 references in the wired install surface at #46's filing -> 0. GATES, both green: smoke-s4 all 54 files smoke-s34 virgin cluster; and its board assertion FLIPPED to fail-closed — a fresh install that has booted but loaded NO KERNEL now refuses task.create, because urn:ckp:board/Task is declared by the kernel TTL and no kernel means no shape means no admitted type. It previously demanded ok:true, which only held because every v3.7 type was waved past the admitted-type check and then took a vacuous conforms:true from a gate targeting nothing. Closes the write-path half of #46 and #47.
…three defects that made it impossible, v0.4.43 `ckp.germinate_kernel` existed but no kernel could ever be germinated. Three independent defects stacked, each masking the next; the top one presented as a SHACL refusal naming a clause the payload plainly satisfied, which is why this read as "the gate is broken" for so long. 1. ckp.validate aimed its scratch graph at live data. The scratch id was `1000000000 + pg_backend_pid()`. pgrdf allocates data graphs from that same band: measured on the bench, 59 live graphs sit at offsets 112..1637 — ordinary container pids — and the CORE ontology is at 1000000221, i.e. a backend with pid 221. Such a backend ran clear_graph against real data; only pgrdf's "bound to a different IRI" check stopped it from clearing core. Every seal from that backend then failed with a governance error unrelated to its payload — intermittent and pid-dependent. The scratch is now allocated BY IRI, outside the data band, one per backend. 2. ckp._body_to_ttl dropped arrays — in the overload seal actually calls. A multi-valued property emitted no triples at all, so no sealed instance could carry one. ckp:Kernel requires three organs, so ckp:Kernel was unsealable: germinate refused with hasOrgan MinCount(3) while the body carried all three. There are two overloads; fixing only the 2-arg sibling changed nothing, because seal calls the 3-arg one. Overload parity is a route, like baseline/upgrade parity. 3. The sealed body and the graph disagreed about where organs live. hasOrgan was derived from v_kid, which already ends in /kernel, yielding <base>/kernel/organ/* while the graph — and pgCK's own kernel — declare <base>/organ/*. KernelShape only counts and type-checks them, so the gate passed and a Kernel was sealed pointing at three resources that do not exist. Both now derive from one v_base. Not ours, recorded because it shaped the hunt: pgrdf caches term→id in shared memory and does not invalidate on abort. After a failed germination the dictionary row is rolled back but the cache entry survives, so later parses write quads referencing a phantom id. The quad stores; SHACL joins the dictionary and cannot see it. Net effect: a failed germination permanently poisoned the exact IRIs the next attempt needed, so a kernel name could never be created again. pgrdf.shmem_reset() clears it. Worth an upstream report. Verified end to end: ck-lib-js germinated through ckp.dispatch, three organs with counted dependencies 0/1/2, ledger and proof written, ownedBy stamped from the connection; anonymous callers and NATS-metacharacter names still refused; a cold-start kernel with no prior graph germinates. Store-wide dangling references back to 0. Gates: smoke-s4 exit 0 (70 PASS), smoke-s34 PASS.
… none — v0.4.44
Two divergences of the same kind: one fact represented in two places, with
nothing checking they agree, each resolving toward the permissive side.
IDENTITY. ckp.seal resolved the participant from p_body->'participant'->>'sub'
— the payload — while germination stamped ownedBy from `ckp.requester`, the
transaction-local GUC the relay sets from the callout-verified connection. One
seal therefore carried two identities. Measured by pgCK.MCP over a verified
connection:
ckp:ownedBy urn:ckp:participant:f767f1b7-…
ckp:createdBy urn:ckp:participant:anon:0e93c962-…
"Owned by someone, created by nobody." Worse than the mismatch: createdBy was
client-assertable, which is what the four InstanceShape stamps exist to prevent.
The verified connection now wins; a conflicting payload sub is ignored, never
merged. The payload arm survives only where no verified connection exists.
task.create / instance.create / notify already did this in their handlers
(s58); seal was the last path still trusting the body, which is why germination
alone sealed anon.
This closes forgery THROUGH THE DOOR, where only the relay sets the GUC. It
does not make direct SQL trustworthy — a SQL caller sets that GUC by hand. The
door is the boundary; SQL access was always above it.
AUTHORIZATION. `affordances` reports the SEALED ckp:Affordance set for the
calling kernel, filtered on the unforgeable producedBy stamp, correctly
returning [] when nothing is declared. Dispatch authorized from a DIFFERENT set
(ckp.affordance_registry), so an empty affordance list did not mean an empty
surface. Measured: pgck-mcp 0.2.32 slugged its kernel segment pgCK -> pgck; all
26 registry rows name pgCK, so affordances went 3 -> EMPTY while dispatch
carried on. Fail-open authorization, d-28-sah-1.
Empty now means none, with ONE bootstrap exception written down as an exception
rather than smuggled in as a fallback: kernel.germinate, because a kernel that
does not exist yet cannot own a registry row. It refuses anonymous callers and
stamps ownedBy from the verified connection, so reaching it proves an identity
rather than bypassing one.
ROUTE PARITY. ckp.affordances_of and ckp._affordance_schema existed only in the
0.4.35--0.4.36 migration, never in the baseline, while dispatch has called
affordances_of since 0.4.36 — so a FRESH install carried a verb that raises
"function does not exist". Invisible because the suite reaches `affordances`
only on an upgraded substrate. Both routes now carry identical text.
Also recorded: the substrate-wide registry widening measured earlier in this
work was an uncommitted live patch on the bench, not a substrate property. It
is superseded by the fail-closed rule above.
Gates: smoke-s4 exit 0 (68 named tests), smoke-s34 exit 0.
…self — v0.4.45 A project name is ONE transport segment, lowercase, dashes optional. `pgCK` is the repo and display name; it was never a valid project name. pgck-baseline.sql hard-coded 'pgCK' in 37 places: all 26 affordance_registry seed rows, their input.kernel.pgCK.action.* subjects, the event.kernel.pgCK.*.sealed subject, and the kernel_epoch lookups inside ckp.apply / bump_epoch / compile_plans / concept_match. The SUBSTRATE published its own verbs under a name no conforming client can produce. Presentation: a client that correctly slugs its workspace publishes to input.kernel.pgck.action.*, the relay sets ckp.project = 'pgck', every registry row says 'pgCK', and affordances returns EMPTY. I read that as a client regression and reported it as one. It was the seed. Registry rows and kernel_epoch are hand-registered routing state, not sealed facts, so 0.4.45 repoints them in place (idempotent — a substrate installed from the corrected baseline seeds 'pgck' and matches zero rows). Sealed ckp:Affordance instances carry producedBy urn:ckp:pgCK/kernel/ck and are NOT rewritten: a seal is precisely what cannot be edited. They are superseded when the canonical kernel materializes its epoch. germinate now requires the canonical form. The old guard tested only for NATS metacharacters, letting ':' and '/' through, so a caller passing a project URN built <urn:ckp:urn:ckp:project:ck-lib-js/kernel/ck> — 22 asserted triples on the bench, structurally valid, addressable by nobody, refused nowhere. Gates: smoke-s4 exit 0, smoke-s34 exit 0.
…n — v0.4.45 ckp.validate_instance validated candidates against <urn:ckp:<proj>/kernel/ck> — the kernel graph, which holds a Kernel and three organs and NO shapes at all. Measured on the bench: 30 triples and 0 sh:targetClass, against a composed surface of 1258 triples and 27 targets. No focus node could ever be selected, so every verdict was structurally vacuous. The only reason that surfaced as an error instead of a confident conforms:true is pgrdf.validate's no-target guard. It even computed ckp._composed_shapes() for the admitted-type check, then threw it away with the enclosing DECLARE block's scope. Three axes diverged from seal; all three now match: shapes graph kernel/ck -> _composed_shapes() property map kernel/ck -> <proj>/shapes/composed serializer _body_to_ttl/2 -> _body_to_ttl/3 (datatypes from the shapes) The serializer axis is the one that would have bitten quietly: /2 resolves no declared datatypes and, before 0.4.44, dropped arrays outright — so validate could pass a body that seal then refused on cardinality, or vice versa. Until now "validate PREDICTS seal" (pgCK#27) held only for the admitted-type half — the half that carries its own explicit check and never consulted a shape. Found while trying to learn wave:Ticket's required fields the sanctioned way, via ck_validate, which is precisely the tool this defect made useless. Gates: smoke-s4 exit 0, smoke-s34 exit 0, named test set identical to the prior green run (42/42, nothing dropped).
…4.46
THE ONE LINE THAT EXPLAINS THE REST. ckp.dispatch resolved every caller's
affordances under a fixed name:
v_aff := ckp.registry_lookup('pgck', v_canon);
A verb registered by any other kernel was invisible, so every workspace but one
got unknown_affordance. It looked correct only because the seed and the
registrars were hard-coded to the SAME literal — writer and reader wrong in the
same direction, which is not agreement, only symmetry. Correcting one side is
what made it visible (smoke-s4 s41: registered under 's41-test', resolved under
'pgck').
The same split, at six more depths:
dispatch registry_lookup('pgck', …) -> ckp._project()
registry_lookup caller only -> caller first, seeded floor second
plan readers x3 WHERE kernel = 'pgck' -> caller first, seeded floor second
plan registrars x2 INSERT 'pgck' -> p_project (they were HANDED it)
apply bump_epoch('pgck') -> bump_epoch(v_proj)
project resolution x13 inline, TWO spellings -> ckp._project()
ckp.apply bumping a fixed kernel meant every other kernel's epoch never
advanced and its plans were recompiled under a name it does not own — which is
why a freshly germinated kernel sat at epoch 0 no matter what it did.
The thirteen inline resolutions disagreed on the empty string: dispatch mapped
'' to '', everything else to 'demo', so an empty GUC sent the lookup and the
write to DIFFERENT kernels. They now share ckp._project(). The 'demo' fallback
is itself a real kernel name and therefore a landing site for writes belonging
to nobody; it exists in exactly one function now and can be made fail-closed in
one edit instead of thirteen.
PLAN AND AFFORDANCE RESOLUTION IS TWO-LAYERED, and must be: resolving only the
caller made the SEEDED substrate verbs unreachable for everyone (s32 —
concept.match is seeded at install and belongs to no kernel, because install
runs before any kernel exists); resolving only the fixed kernel made every
project share one name's surface. The caller's own row wins; the seeded floor
is the fallback.
Also here, found while sealing the first integrity-check affordance:
- P0-E's second half: add_affordance whose DETAIL projects nothing is refused
at propose. It checked the OP had a projector, never that the detail carried
anything, so an empty detail sealed `applied`, bumped the epoch, registered
nothing and returned ok:true with no error anywhere. pgck epoch 5 IS that
inert applied — an epoch that logged nothing while reporting success.
- propose_change read p_payload->'detail' while apply reads 'proposalDetail';
a caller using the name it reads back silently got {}. Both accepted.
- `about` defaulted to the bare project segment where ProposalShape demands an
IRI, so every proposal omitting `about` was refused. The refusal was correct
and useful; the default was not.
- vote read only 'value' while VoteShape declares ckp:voteValue. Both accepted.
Tests: s23 asserts on 'pgck' because it drives that kernel explicitly at every
call; s27 asserts on ckp._project() because it goes through dispatch. Those are
different contracts and were conflated.
The registry SEED still hard-codes 30 rows. That is the honest remainder: install
runs before any kernel exists, so the seed has nothing else to name. Making those
verbs belong to no kernel is the substrate-floor change, not this one.
Gates: smoke-s4 exit 0, smoke-s34 exit 0, named test set identical to the prior
green run (42/42). Six intermediate gate failures, each a real defect, none
shipped.
0.4.46 fixed ckp.dispatch to resolve the CALLER's affordances, then shipped the
wrong function. There are two overloads:
dispatch(p_verb, p_kernel_urn, p_payload, p_identity) -- thin wrapper
dispatch(p_verb, p_payload) -- the routing logic
The migration generator matched on the NAME PREFIX and took the first, so the
wrapper was re-created and the routing function kept registry_lookup('pgck',…).
The baseline carried the fix; the upgrade route did not.
Reported by pgCK.MCP, re-measured here in the exact relay conditions:
set_config('ckp.project','pgck-mcp', true)
ckp._project() = pgck-mcp (NOT 'demo')
registry_lookup('pgck-mcp','surface.targets') = plane=query, epoch=2
verb_canon('surface.targets') = surface.targets
dispatch('surface.targets') = unknown_affordance
So a verb a kernel registered, voted through and applied could not be called by
that kernel. Their reading — that ckp._project() falls back to 'demo' — is
wrong: it returned pgck-mcp correctly. The fault was the overload, and the
distinction matters because "the project resolver is broken" would send the
next reader to the wrong function entirely.
After 0.4.47, both answer:
dispatch as pgck-mcp -> surface.targets ok:true, rows
dispatch as pgck -> integrity.organs ok:true, rows
Cross-kernel dispatch works for the first time.
GATE GAP, worth more than this fix: smoke-s4 and smoke-s34 BOTH install from the
baseline, which had the fix — so both passed while the migration shipped the
wrong function. Nothing exercises the migration chain. A migration can ship the
wrong overload, or omit a function entirely, and the whole suite stays green;
the bench is the only place that path runs, and it runs unverified. This is the
third overload-parity failure today (_body_to_ttl, the registry pair, dispatch)
and the first one no test could have caught.
Gates: smoke-s4 exit 0, smoke-s34 exit 0, named set identical to prior green
(42/42) — which is exactly the point: they would have been green either way.
ckp._op_to_ttl's add_class arm never read detail.properties. It emitted one quad
(<class> a owl:Class), reported graph_changed:true / applied_quads:1, and
dropped every declared property shape without complaint. Reported by pgCK.MCP
against urn:ckp:pgck-mcp/type/ToolProjection: two property shapes sent, one quad
applied, nothing validatable. Same family as detail/proposalDetail and the inert
epoch — a payload key the door does not read.
ONE CORRECTION to the report, and it makes the defect worse. The claim was that
the resulting type "cannot be sealed against, because seal requires a shape to
target it". It does not. ckp._type_admitted accepts
{ ?s sh:targetClass <T> } UNION { <T> a rdfs:Class } UNION { <T> a owl:Class }
so `a owl:Class` admits the type by itself. The type is sealable immediately and,
with nothing targeting it, every instance validates VACUOUSLY. add_class did not
govern a dead type into existence — it governed a vacuity generator into
existence, at a sealed epoch, through the sanctioned door.
properties[] now project as a sh:NodeShape targeting the class, with the same
per-property gate add_property applies (path IRI required, minCount integer,
datatype IRI). A malformed property is REFUSED at propose, never dropped:
silently narrowing an enforcement surface is un-enforcement nobody can see.
NOT CHANGED, deliberately: bare add_class still emits the class alone. Five smoke
tests use it that way as a projectored op for the governance loop, and it is a
legitimate building block before an add_property. The vacuity window stands for
as long as no shape targets the new class. Closing it means either refusing bare
add_class or making _type_admitted demand a TARGETING shape rather than accepting
`a owl:Class` — a doctrine decision about what "admitted" means, not a projector
fix, and not one to take unilaterally.
Gates: smoke-s4 exit 0, smoke-s34 exit 0, named set identical to prior green.
…seeded floor second
Nothing in the suite dispatched as more than one kernel, so ckp.dispatch could
resolve EVERY caller's affordances under one fixed name and stay green:
v_aff := ckp.registry_lookup('pgck', v_canon); -- until 0.4.46
A verb a kernel registered, voted through and applied was invisible to its own
owner, and both gates passed. 0.4.47 then shipped the wrong dispatch OVERLOAD
with both gates still green, because both install from the baseline.
Four claims, none of which had a test:
(a) a kernel resolves the verb IT registered
(b) a kernel does NOT resolve another kernel's verb <- fail-closed
(c) a kernel owning nothing still reaches the SEEDED floor
(d) the substrate seeds its own surface under a canonical name
(b) is the one that would have been missed by a naive fix: making resolution
work for the owner (a) is satisfiable by opening it to everyone. (b) is what
makes the fix fail-closed rather than merely working.
(c) and the floor fallback are both load-bearing and were found the hard way:
resolving only the caller made the seeded substrate verbs unreachable for every
project (the s32 failure); resolving only the fixed kernel made every kernel
share one name's surface.
(d) is scoped to the SEEDED verbs, not every registry row. Its first run failed
on s17/s18's synthetic 'TestK' — a legitimate fixture, since registry_lookup
correctly does not care about case. The claim it can defend is narrower: the
substrate must not publish ITS OWN surface under a name no conforming client can
address, because NATS subjects are case-sensitive and a correct client slugs its
workspace. A vacuity guard fails the assertion loudly if no seeded row exists,
rather than passing because it selected nothing — three of today's defects had
exactly that signature.
Gates: smoke-s4 exit 0, smoke-s34 exit 0; the prior 42 named tests unchanged.
…ealed them — v0.4.49
Measured while chasing "subscribers see messages in the browser":
ckp.outbox 0 rows (drain works, trigger fires)
registry out_topic populated 0 of 30
sealed Affordance outTopic 0 of 5
compute_publish_subject() -> event.kernel.pgCK.Kernel.sealed
by header keyed on urn:ckp:board/created_by (5 of 78 instances;
73 carry the core createdBy)
1. EVERY KERNEL PUBLISHED TO ONE SUBJECT, AND IT WAS THE WRONG ONE.
compute_publish_subject hard-coded the kernel segment. Worse, 0.4.45
canonicalized it in the BASELINE and no migration ever carried it, so the
bench has been publishing to event.kernel.pgCK.* — uppercase — which no
conforming client subscribes to.
This is the same defect as dispatch resolving every caller under one name
(0.4.46/47), on the OUTBOUND path, where it is far harder to notice:
inbound, a wrong kernel segment answers unknown_affordance and someone files
it in minutes; outbound, the publish SUCCEEDS to a subject nobody listens on
and is indistinguishable from "no events yet". That is why real push was
long-awaited rather than long-reported.
The kernel is now derived from the SEALED producedBy stamp
(urn:ckp:<project>/kernel/ck), not session state, which by trigger time may
belong to a different caller.
2. `by` WAS ABSENT ON 73 OF 78 INSTANCES. The header keyed only on the v3.8-era
urn:ckp:board/created_by; after 0.4.44 the derived stamp is ckp:createdBy in
core. A message with no sender is not a message a peer can act on. Reads the
core stamp first, board form as legacy fallback; unforgeable either way,
because createdBy derives from the verified connection.
3. out_topic DECLARED NOTHING — NULL on all 30 rows, so a subscriber had to read
src/nats_client.rs to learn where replies go. Registration now states the
truth the transport already implements: result.kernel.<kernel>.<verb>. No new
channel.
OVERLOAD HAZARD, caught before shipping: adding a DEFAULT parameter does NOT
replace compute_publish_subject(text) — it creates a second function beside it,
and a one-arg call then binds to the old hard-coded one or fails ambiguous. The
DEFAULT chosen specifically to avoid an overload pair is what would have created
one. The migration DROPs the 1-arg form first. Fourth overload-parity defect
today, and the first where the avoidance mechanism was the cause.
Verified end to end (sealed in a transaction, read from the outbox, rolled back):
event.kernel.ck-dev.Project.sealed by: urn:ckp:participant:d6b635a3-…
event.kernel.ck-dev.Kernel.sealed by: urn:ckp:participant:d6b635a3-…
NOT verified: NATS delivery itself. Only that the outbox row is addressed
correctly. The drain is a separate link and CK-dev's browser is its test.
Gates: smoke-s4 exit 0, smoke-s34 exit 0, 42 named tests + s63 unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the substrate to CKP v3.11 and closes the defects that made a concept kernel
uncreatable, unaddressable, and unattributable.
What now works that did not
kernel.germinateseals a Project and Kernel withthree organs and counted dependencies,
ownedBystamped from the verified connection.ckp.dispatchresolved every caller's affordances under onefixed name, so a verb a kernel registered, voted through and applied was invisible to its
own owner. No
ckpfunction names a kernel any more.createdByderives from the connection, not the payload — aseal used to carry two identities (
ownedByverified,createdByanon).carried no
byheader on 73 of 78 instances.integrity.organs,integrity.naming,proposed/voted/applied and callable through the one door.
The pattern
Nearly every defect was one fact named in two places, agreeing only by accident, and
resolving toward the permissive side when they diverged. Correcting one half is what made
the other visible — four were overload pairs, where
CREATE OR REPLACEmatched the wrongsignature.
Verification
smoke-s4andsmoke-s34green; CI green (fmt, clippy, cargo test, pgrx test on pg18).New
s63_kernel_resolutionasserts the four claims that had no test: a kernel resolves itsown verb, does not resolve another's, still reaches the seeded floor, and the substrate
seeds its own surface under a canonical name.
Known gaps, deliberately not closed here
0.4.47 shipped the wrong overload and
compute_publish_subjectsat stale on the bench.pending a pass over test fixtures that seal unshaped types.