Skip to content

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
styk-tv merged 20 commits into
mainfrom
feat-namespace-publication-c
Aug 12, 2026
Merged

v3.11: kernel creation works, every function resolves its own kernel, and events name who produced them — 0.4.36→0.4.49#62
styk-tv merged 20 commits into
mainfrom
feat-namespace-publication-c

Conversation

@styk-tv

@styk-tv styk-tv commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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 creation through the door. kernel.germinate seals a Project and Kernel with
    three organs and counted dependencies, ownedBy stamped from the verified connection.
  • Cross-kernel dispatch. ckp.dispatch resolved every caller's affordances under one
    fixed name, so a verb a kernel registered, voted through and applied was invisible to its
    own owner. No ckp function names a kernel any more.
  • Identity has one source. createdBy derives from the connection, not the payload — a
    seal used to carry two identities (ownedBy verified, createdBy anon).
  • Events reach their kernel, with a sender. Publishes went to one hard-coded subject and
    carried no by header on 73 of 78 instances.
  • Governed integrity checks as affordancesintegrity.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 REPLACE matched the wrong
signature.

Verification

smoke-s4 and smoke-s34 green; CI green (fmt, clippy, cargo test, pgrx test on pg18).
New s63_kernel_resolution asserts the four claims that had no test: a kernel resolves its
own 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

  • CI does not run the SQL suite. All 43 tests run locally only.
  • Nothing tests the upgrade route. Both gates install from the baseline, which is how
    0.4.47 shipped the wrong overload and compute_publish_subject sat stale on the bench.
  • NATS delivery unproven — the outbox row is correctly addressed; arrival is untested.
  • A vacuity refusal (seal refuses what no shape would judge) is written and parked,
    pending a pass over test fixtures that seal unshaped types.

Peter Styk 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.
@styk-tv styk-tv changed the title v3.11 only: the door carries the kernel-URN, a refusal no longer closes it, and ontology/ holds v3.11 alone — 0.4.36→0.4.41 v3.11: kernel creation works, every function resolves its own kernel, and events name who produced them — 0.4.36→0.4.49 Aug 12, 2026
@styk-tv
styk-tv merged commit c5c546e into main Aug 12, 2026
4 checks passed
@styk-tv
styk-tv deleted the feat-namespace-publication-c branch August 16, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant