Uh oh!
There was an error while loading. Please reload this page.
test(objectql): measure the ADR-0130 cross-package matrix over nine object-naming item classes, navigationContributions group semantics and analytics binding (#14454) - #14549
Conversation
…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
📓 Docs Drift CheckNothing 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
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
hotlong
commented
Sep 2, 2026
Review — PASS (measurement only, Clause-② NO)Disclosure: reviewer and dispatcher are the same session (
Flipping ready and arming auto-merge (MERGE). The matrix folds into #14122 §4 once this lands. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
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 bootedObjectKernel, the same seamregistry-artifact-co-ownership.test.tsdrives. A ownscrm_account; B ownscrm_invoiceand namescrm_accountfrom 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:
defineStack→validateCrossReferences(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 isstack.zod.ts's, verbatim.registerApp→installPackage(packages/objectql/src/engine.ts:4797). It validates no object reference on any of these classes; everything registers throughregisterMetadataCollections.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:lineare inpackages/objectql/src/registry-cross-package-item-classes.test.ts.objectNameAction 'NAME' references object 'OBJECT' which is not defined in objects.— nocode, nostatus:282/:316data.objectView[0].list references object 'OBJECT' which is not defined in objects.— no code/status:335object:384/:434regions[].components[].dataSource.object:448object:392/:434object:410/:434objectsPermission 'NAME' grants on object 'OBJECT' which is not defined in objects.— no code/status:290/:316objectSeed data references object 'OBJECT' which is not defined in objects.— no code/status:298/:316targetObjectMapping 'NAME' targets object 'OBJECT' which is not defined in objects.— no code/status:306/:316config.objectNameconfigis an openz.record:477dataset:529dataset:5515 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:
: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.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.: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[].groupfile:lineare inpackages/objectql/src/registry-nav-contribution-group-semantics.test.ts.groupresolves against a group node the target app declaresidandtype === 'group', appended into that group's children, not duplicated at top level:143,:158:170,:186,:245priority, not registration order: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.code/status, noconsole.warn, noconsole.error(:186asserts both channels empty);console.logfrom the registry's privatelog(), gated atlogLevelinfo/debug. The shipped default isinfo, so a default boot prints it once — but atwarn, which is exactly whatOS_REGISTRY_LOGexists to select (and what this package's ownvitest.config.tsselects, finding: test-suite console chatter measured at 61k lines per full repo run — dogfood 41k and objectql 5k dominate, and the loudness is a logger-default question #13517), the relocation is completely silent while still happening;:245):groupnames a node in an app the contributing package does not own, andnavigationContributionsappears in no cross-reference rule at all. A typo'd group id survives both doors.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:158shows the same path is reached by a correctly spelled id that names anobjectnode rather than agroupnode.⛔ 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/:186are 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(...)fromdefineStack—codeandstatusareundefined. 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 bareError), 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/objectqlsuite —pnpm --filter @objectstack/objectql test→ 259 files / 4479 tests passed, exit 0. (The 22 new pins are in that total; run alone they are 22/22.)Typecheck —
pnpm --filter @objectstack/objectql typecheck→ exit 0. Both new files confirmed inside the program viatsc -p tsconfig.test.json --listFiles(2 hits) — a clean typecheck that excluded them would have said nothing about them.Gate union —
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderived 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). Afterpnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'(70/70 tasks):pnpm check:type-check-debt→ exit 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-loads→ exit 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 savedturbo run testlog; 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 baregit diff --stat), each restore proven by blob hash equal to the HEAD blob plus an emptygit diff HEAD,trap … EXIT INT TERMwith absolute paths:prioritysort inapplyNavContributionsPROPOSITION 3 (HOLDS)PROPOSITION 1 (scope), which rides the same pathsharingRulesfromMETADATA_ARRAY_KEYSsharing 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 throughdist. 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 —⚠️ 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.
skip-changesetapplies.scripts/check-empty-changeset.mjsnames "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)."🤖 Generated with Claude Code
https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
Generated by Claude Code