Part of #14122 (measured by #14454 item 2, pinned in PR #14549).
What was measured
SchemaRegistry.applyNavContributions (packages/objectql/src/registry.ts, the else branch under if (c.group)): when a package contributes navigation items into a group id that the target app does not declare, the items are appended at the app's top level. Nothing throws, nothing carries an ADR-0112 code/status, and there is no console.warn / console.error. The only trace is one this.log(...) line, gated at logLevelinfo/debug. At OS_REGISTRY_LOG=warn — the level the objectql test config selects (#13517) — the relocation is completely silent while still happening.
The authoring door cannot catch it either: navigationContributions appears in no validateCrossReferences rule, and the group id names a node in an app the contributing package does not own.
Pins (record today's behaviour, no runtime change): packages/objectql/src/registry-nav-contribution-group-semantics.test.ts — PROPOSITION 2 (DOES NOT HOLD AS POSED), PROPOSITION 2 (visibility), PROPOSITION 2 (authoring door). Ablation on #14549 confirmed the pins go red when the branch drops items instead of relocating.
Why it matters
#14122 §4 feared a vanishing contribution. A relocated one is worse for the named consumer (hotcrm's 17-node navigation conversion, hotcrm#1449): the entry is present, a smoke test passes, and the information architecture has silently changed. A typo'd group id is exactly the error an AI author emits.
Options
| Option | Cost | What it buys |
|---|
| A | Refuse at install: reject the contribution with an ADR-0112 envelope when the target app declares no such group | Introduces an ordering constraint — registerAppNavContribution deliberately does not require the target app to exist yet, and the merge is a read-time fold precisely so registration order does not matter; also breaks any package contributing into an optional group | The loudest signal, before any user sees a moved menu |
| B | Keep relocating, upgrade the trace to console.warn (or a real diagnostic) so it survives OS_REGISTRY_LOG=warn | One-line change to an existing log call; no new refusal, no ordering constraint | The failure is noticed at the point it happens; still not a gate |
| C | No platform change; hotcrm's conversion asserts its own merged nav tree equals the pre-split tree | A test in the consumer repo | Catches the regression for the one measured consumer only |
Four-axis reading (PM seat, for the maintainer to confirm or overrule)
- 平台长期一致性 — A is the contract-first shape in the abstract, but it trades away the order-independence the read-time fold was designed for; B keeps the design and fixes the observability gap.
- 实测业务拉力 — the only measured consumer (hotcrm 17-node conversion) needs the mis-aim to be noticed; B delivers that at the point of failure, C delivers it in the consumer's own suite.
- AI agent 抗错性 — a typo'd group id today yields a silently different IA; B makes it visible without inventing a refusal an author must learn to suppress.
- 创业阶段范围纪律 — B changes one log call, C adds one consumer test; A adds a new refusal surface and a registration-order rule.
Recommendation: B now, C alongside in hotcrm. If the maintainer wants the gate rather than the signal, A is the shape — and it should then also cover the app-does-not-exist case, which is equally unchecked today.
Not release-gating for the ADR-0130 chain: the behaviour is unchanged from before the epic and no packaged product depends on navigationContributions yet.
Part of #14122 (measured by #14454 item 2, pinned in PR #14549).
What was measured
SchemaRegistry.applyNavContributions(packages/objectql/src/registry.ts, theelsebranch underif (c.group)): when a package contributes navigation items into agroupid that the target app does not declare, the items are appended at the app's top level. Nothing throws, nothing carries an ADR-0112code/status, and there is noconsole.warn/console.error. The only trace is onethis.log(...)line, gated atlogLevelinfo/debug. AtOS_REGISTRY_LOG=warn— the level the objectql test config selects (#13517) — the relocation is completely silent while still happening.The authoring door cannot catch it either:
navigationContributionsappears in novalidateCrossReferencesrule, and the group id names a node in an app the contributing package does not own.Pins (record today's behaviour, no runtime change):
packages/objectql/src/registry-nav-contribution-group-semantics.test.ts—PROPOSITION 2 (DOES NOT HOLD AS POSED),PROPOSITION 2 (visibility),PROPOSITION 2 (authoring door). Ablation on #14549 confirmed the pins go red when the branch drops items instead of relocating.Why it matters
#14122 §4 feared a vanishing contribution. A relocated one is worse for the named consumer (hotcrm's 17-node navigation conversion, hotcrm#1449): the entry is present, a smoke test passes, and the information architecture has silently changed. A typo'd group id is exactly the error an AI author emits.
Options
registerAppNavContributiondeliberately does not require the target app to exist yet, and the merge is a read-time fold precisely so registration order does not matter; also breaks any package contributing into an optional groupconsole.warn(or a real diagnostic) so it survivesOS_REGISTRY_LOG=warnFour-axis reading (PM seat, for the maintainer to confirm or overrule)
Recommendation: B now, C alongside in hotcrm. If the maintainer wants the gate rather than the signal, A is the shape — and it should then also cover the app-does-not-exist case, which is equally unchecked today.
Not release-gating for the ADR-0130 chain: the behaviour is unchanged from before the epic and no packaged product depends on
navigationContributionsyet.