Skip to content

test(objectql): measure the ADR-0130 cross-package matrix over nine object-naming item classes, navigationContributions group semantics and analytics binding (#14454) - #14549

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-14454-cross-package-matrix
Sep 2, 2026
Merged

test(objectql): measure the ADR-0130 cross-package matrix over nine object-naming item classes, navigationContributions group semantics and analytics binding (#14454)#14549
hotlong merged 1 commit into
mainfrom
claude/issue-14454-cross-package-matrix

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14454
Part of #14122

Items 1, 2 and 4 of the card. Item 3 (permission-set module ownership) was decided by the maintainer on 2026-09-02 (option B) and split to #14487 / #14488 — nothing here designs for it.

Measurement only: no runtime behaviour changes, no source files touched. Two new test files, 22 pins.

Method

Same as #14122 §4, run at the registry/engine level: package A and package B installed as same-artifact co-owners of one namespace (ADR-0130 D1, installPackage(manifest, settings, scope) from #14354) through the real load path — manifest.register({ packages: [...] }) on a booted ObjectKernel, the same seam registry-artifact-co-ownership.test.ts drives. A owns crm_account; B owns crm_invoice and names crm_account from the item class under test.

Every row reads TWO doors, and that is the finding that reorganises the matrix. §4 reported four verdicts without saying which door produced them, and they are not from the same one:

  • Authoring gatedefineStackvalidateCrossReferences (packages/spec/src/stack.zod.ts:1109). It sees one stack, so a co-owner's object is indistinguishable from a typo. This is where §4's two REFUSE rows came from: their message text is stack.zod.ts's, verbatim.
  • Install gateregisterAppinstallPackage (packages/objectql/src/engine.ts:4797). It validates no object reference on any of these classes; everything registers through registerMetadataCollections.

Reading only the install gate would report a uniform "ACCEPTED" for all nine classes — true, and useless. Reading only the authoring gate would miss that the runtime enforces nothing. So the matrix verdict is the effective one: refused at authoring means the module cannot be written, whatever the registry would have done.

Matrix — item 1 (nine object-naming classes) and item 4

file:line are in packages/objectql/src/registry-cross-package-item-classes.test.ts.

# Item class Key Verdict Error / code Pin
1 action objectName REFUSED at authoring · ACCEPTED at install Action 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status :282 / :316
2 view data.object REFUSED at authoring · ACCEPTED at install View[0].list references object 'OBJECT' which is not defined in objects. — no code/status :335
3a page (record) object ACCEPTED (unenforced) both doors — nothing checks :384 / :434
3b page (related list) regions[].components[].dataSource.object ACCEPTED (unenforced) both doors — nothing checks :448
4 dataset object ACCEPTED (unenforced) both doors — nothing checks :392 / :434
5 sharing rule object ACCEPTED (unenforced) both doors — nothing checks :410 / :434
6 permission set objects REFUSED at authoring · ACCEPTED at install Permission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status :290 / :316
7 seed dataset object REFUSED at authoring · ACCEPTED at install Seed data references object 'OBJECT' which is not defined in objects. — no code/status :298 / :316
8 import mapping targetObject REFUSED at authoring · ACCEPTED at install Mapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status :306 / :316
9 flow node START node config.objectName ACCEPTED (unenforced) both doors — nothing checks; config is an open z.record :477
I4a dashboard widget dataset ACCEPTED — registers under B, A's dataset resolves — nothing checks :529
I4b report dataset ACCEPTED — registers under B, A's dataset resolves — nothing checks :551

5 REFUSED, 7 ACCEPTED. Item 4's answer to "can a module carry its own dashboard?" is yes — a dashboard widget and a report both bind a dataset defined in the other package, at both doors.

Each row carries its own discriminator, so no row is a phantom green:

  • REFUSED rows also assert a control: the identical fixture with the object declared locally is ACCEPTED (:316). Without it the row could not tell "the boundary refuses this" from "my fixture is malformed" — which is exactly what my first two fixture drafts were.
  • ACCEPTED (unenforced) rows also assert that a dangling name (crm_nowhere, ds_nowhere) is accepted identically, and read the item back out of the registry: stamped _packageId = B, foreign reference intact, resolveObject('crm_account') = A's definition, getNamespaceOwners('crm') = both. "Registers and resolves" is asserted as two halves because either alone is satisfiable by accident.
  • Continuity control (:236): §4's rule R4 (hooks[].object) is re-measured with this method and reproduces §4's message verbatim. If that row ever disagrees with §4, the nine new rows are measuring something else and must not be folded in.

Matrix — item 2, navigationContributions[].group

file:line are in packages/objectql/src/registry-nav-contribution-group-semantics.test.ts.

Proposition Verdict Pin
1. group resolves against a group node the target app declares HOLDS — DFS by id and type === 'group', appended into that group's children, not duplicated at top level :143, :158
2. a contribution into a group that does not exist fails VISIBLY rather than vanishing ⚠️ NEITHER — see below :170, :186, :245
3. several packages into one group are ordered by priority, not registration order HOLDS — priority ascending; ties fall back to arrival (stable sort); schema default 200 lands between explicit 100 and 300 :270, :284

🔴 For the PM — proposition 2 is neither confirmed nor refuted as posed, and the third thing is worse than both

The platform relocates the contribution to the app's top level (registry.ts applyNavContributions). It does not vanish, and it does not fail.

Why this matters more than a silent drop. The card feared vanishing, because a missing entry is noticed. A relocated entry is present, looks fine in a smoke test, and has silently changed the information architecture — which is the one property hotcrm's 17-node navigation conversion exists to preserve. PROPOSITION 1 (scope) at :158 shows the same path is reached by a correctly spelled id that names an object node rather than a group node.

⛔ Per the card's fence, no runtime behaviour is changed here — the pins record today's behaviour. If the PM rules that a missing group must refuse (or at minimum console.warn), :170 / :186 are what change, in that card, deliberately.

🟡 Second flag: the authoring gate has no ADR-0112 envelope

All five REFUSED rows are a bare new Error(...) from defineStackcode and status are undefined. The message is the only thing distinguishing one refusal from another, so the pins assert the message and then assert the envelope's absence explicitly, once, at :251. Same shape of gap as #14367 (registerObject's bare Error), one door over. A red there means an envelope arrived — update the pin and the §4 row, do not delete the assertion.

Verification

All at 2019fdc8d; exit codes captured before any pipe (cmd > log 2>&1; EXIT=$?).

  • Full @objectstack/objectql suitepnpm --filter @objectstack/objectql test259 files / 4479 tests passed, exit 0. (The 22 new pins are in that total; run alone they are 22/22.)

  • Typecheckpnpm --filter @objectstack/objectql typecheck → exit 0. Both new files confirmed inside the program via tsc -p tsconfig.test.json --listFiles (2 hits) — a clean typecheck that excluded them would have said nothing about them.

  • Gate unionnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 28 families from the real change set (24 by path + 6 by kind, 2 shared). Harvested with --commands; all 28 run at this commit: 25 exit 0 first pass; 3 returned exit 3 = PREREQUISITE NOT MET (each says so in its own words — not a red, nothing measured). After pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' (70/70 tasks):

    • pnpm check:type-check-debtexit 0: "23 ledger entr(ies) re-measured in 161.3s, 771 raw tsc error(s) total, none above its recorded number. surplus: none" — the ratchet has zero headroom and the new test files add nothing to it.
    • pnpm check:dual-build-cjs-loadsexit 0: 102 require entry points across 66 packages load.
    • node scripts/check-test-completeness.mjs → stays NOT MEASURED: invoked with no argument (as the family names it) it needs a saved turbo run test log; the gate prints "It is not a red, and there is nothing here to fix."
  • Reverse verification (3 ablations, all in packages/objectql/src/) — each mutation proven on disk by counting the removed and injected text (not a bare git diff --stat), each restore proven by blob hash equal to the HEAD blob plus an empty git diff HEAD, trap … EXIT INT TERM with absolute paths:

    Ablation Predicted Observed
    drop the priority sort in applyNavContributions PROPOSITION 3 red 1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocating PROPOSITION 2 red 3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYS the sharing-rule ACCEPTED row red 1 failed / 13 passed — only sharing rule 'object'

    No rebuild was needed and none was done for these: both suites reach the mutated code through relative source imports (./registry.js, ./engine.js), not through dist. That the ablations went red is the proof of that resolution path — a dist-resolved suite would have stayed green, which is the failure mode that makes an ablation lie.

  • Clause ② — NO. No source change, no contract accept/reject behaviour altered, no public surface widened. This PR adds only test files.

Changeset

None, deliberately — skip-changeset applies. scripts/check-empty-changeset.mjs names "tests-only" explicitly as route 2: "delete the changeset and apply the 'skip-changeset' label … an empty changeset is a real input to changesets/action and an all-empty set stalls the release silently and greenly (#4898)." ⚠️ The dispatch prompt asked for "the repo's empty/'no release' form"; the repo's own gate rejects that form. Following the gate, and flagging the conflict rather than picking a side quietly.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…bject-naming item classes, navigationContributions group semantics and analytics binding

Extends #14122 §4's four measured cross-package rules to the nine
object-naming item classes a real product split needs, plus the
`navigationContributions[].group` semantics and the analytics binding.

Same method as §4: two packages installed as same-artifact co-owners of
one namespace through the real load path (`manifest.register()` on a
booted kernel, ADR-0130 D1 / #14354), one pin per item class, accept and
refuse both recorded. Measurement only — no runtime behaviour changes.

Each row reads BOTH doors, because §4's four verdicts do not all come
from the same one: the authoring gate (`defineStack`'s
`validateCrossReferences`, which sees one stack and so cannot tell a
co-owner's object from a typo) and the install gate (`registerApp` →
`installPackage`, which validates no object reference on any of these
classes).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions github-actions Bot added the size/l label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 4a378701156050de0608f728d479c0be038ea836packageMentionDocs.

hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Review — PASS (measurement only, Clause-② NO)

Disclosure: reviewer and dispatcher are the same session (session_01UHvF5hyiZjnCyExFnfQB8m); the maintainer authorized same-session self-review for this epic on 2026-09-02. Claims below were re-verified against the tree at 2019fdc8d, not taken from the PR body.

Flipping ready and arming auto-merge (MERGE). The matrix folds into #14122 §4 once this lands.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 11:50
@hotlong
hotlong enabled auto-merge September 2, 2026 11:50
@hotlong
hotlong added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 1ce8930 Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants