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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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 classKeyVerdictError / codePin
1actionobjectNameREFUSED at authoring · ACCEPTED at installAction 'NAME' references object 'OBJECT' which is not defined in objects.no code, no status:282 / :316
2viewdata.objectREFUSED at authoring · ACCEPTED at installView[0].list references object 'OBJECT' which is not defined in objects. — no code/status:335
3apage (record)objectACCEPTED (unenforced) both doors— nothing checks:384 / :434
3bpage (related list)regions[].components[].dataSource.objectACCEPTED (unenforced) both doors— nothing checks:448
4datasetobjectACCEPTED (unenforced) both doors— nothing checks:392 / :434
5sharing ruleobjectACCEPTED (unenforced) both doors— nothing checks:410 / :434
6permission setobjectsREFUSED at authoring · ACCEPTED at installPermission 'NAME' grants on object 'OBJECT' which is not defined in objects. — no code/status:290 / :316
7seed datasetobjectREFUSED at authoring · ACCEPTED at installSeed data references object 'OBJECT' which is not defined in objects. — no code/status:298 / :316
8import mappingtargetObjectREFUSED at authoring · ACCEPTED at installMapping 'NAME' targets object 'OBJECT' which is not defined in objects. — no code/status:306 / :316
9flow nodeSTART node config.objectNameACCEPTED (unenforced) both doors— nothing checks; config is an open z.record:477
I4adashboard widgetdatasetACCEPTED — registers under B, A's dataset resolves— nothing checks:529
I4breportdatasetACCEPTED — 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.

PropositionVerdictPin
1. group resolves against a group node the target app declaresHOLDS — DFS by idandtype === '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 orderHOLDS — 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.tsapplyNavContributions). 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:

    AblationPredictedObserved
    drop the priority sort in applyNavContributionsPROPOSITION 3 red1 failed / 7 passed — only PROPOSITION 3 (HOLDS)
    missing-group branch drops items instead of relocatingPROPOSITION 2 red3 failed / 5 passed — both PROPOSITION 2 pins + PROPOSITION 1 (scope), which rides the same path
    remove sharingRules from METADATA_ARRAY_KEYSthe sharing-rule ACCEPTED row red1 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

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.

@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

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 queueSep 2, 2026
Merged via the queue into main with commit 1ce8930Sep 2, 2026
39 checks passed
@hotlong
hotlong deleted the claude/issue-14454-cross-package-matrix branch September 2, 2026 12:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@hotlong@claude