Skip to content

Commit 7b8bcfe

Browse files
docs(adr): ADR-0141 says the build refuses a lookup into another artifact and only the save door honours a declared dependency (#27812)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DHpDhX1q4f9FeSSfiSgMm4F)_ Fixes #27616 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只改 ADR-0141 的文字。D1、Consequences 和执行计划第 4 条原来说 `os validate` / `os build` / `os lint`「对跨包绑定两边都不判」;现在写成代码的实际做法:构建时看不到制品之外的依赖包的对象,lookup 指向这种对象会被报错拒绝,流程的触发对象指向它只给警告;声明的依赖只在运行时保存门生效(流程触发对象、数据集主对象、列表视图对象),保存门不判 lookup。 - **为什么改:** ADR-0141 是跨应用依赖的依据文本。旧句让读者(人或 AI)以为指向依赖包对象的 lookup 能过 `os validate`,实际会被拒;#25805 的分析就在这句上错过一次。按 #25805 的裁决(字母 D,暂缓)只改文字,不改行为。 - **风险与回滚:** 零代码、零行为变化,不发版;回滚就是 revert 这一个 commit。 - **先看:** `docs/adr/0141-inter-app-dependency-declared-checked-before-install.md` 的 D1 末段(主改动),其次 Consequences 的 Unchanged 一行和执行计划第 4 条。 - **两分钟验证:** 先 `pnpm exec turbo run build --filter=@objectstack/lint`,再跑 `pnpm --filter @objectstack/lint exec vitest run src/validate-object-references.test.ts src/runtime-gate.package-closure.test.ts`,预期全绿:「NON-DEGENERACY — a name NO package in the artifact provides still errors」钉住 lookup 报错,「the CLI reading is unchanged: no scope stated, the advisory the rule always gave」钉住流程触发对象只警告。 - **需要你做的:** 暂无。contract review owed:席位的契约复审还没做,复审 PASS 之前不求批。 ## What changes One file, text only: `docs/adr/0141-inter-app-dependency-declared-checked-before-install.md`, +10 / -6 (net +4 lines). - **D1, last sentence.** Was: "`os validate`, `os build` and `os lint` keep judging neither side of a cross-package binding: a stack there is one app and cannot see its dependencies' objects." Now: the three commands cannot see the objects of a dependency the artifact does not carry; a stack there resolves against its own objects, its artifact's `packages[]` and the platform's names, so a lookup to such an object is refused (`object-reference-unknown`) and a flow triggered on one warns (`flow-trigger-unknown-object`). Only the save door honours the declaration, for a flow's trigger, a dataset's base and a list view's object; it does not judge a lookup. Closing that gap at build is deferred by the ruling on #25805. - **Consequences, Unchanged.** "the compile-time boundary (`os validate` judges neither side of a cross-artifact binding)" becomes "the compile-time boundary (`os validate` refuses a lookup into another artifact, D1)". - **Execution plan item 4.** The docs state that `os validate` cannot see the objects of a dependency the artifact does not carry, "so it refuses a lookup to one and warns on a flow triggered on one (D1)". - **Evidence header.** One dated sentence: the three build-time sentences were re-read at `f5dd057587` and corrected on 2026-10-30 (#27616). The `**Status**:` line is unchanged (`Proposed`). No decision moves, no behaviour changes, no changeset: `docs/adr/**` publishes nothing (`skip-changeset`). ### What the tree does (read at `origin/main` `f5dd057587`) - **Build, lookup target.** `packages/lint/src/validate-object-references.ts`: `validateObjectReferences` resolves a name against the stack's own objects plus `artifactProvidedObjectNames` (the bodies in `packages[]`; its docblock: "Nothing else is read off the entry: not its id, not its dependencies"), then `isPlatformProvidedObjectName`. An unresolved name without a platform prefix (`PLATFORM_OBJECT_PREFIXES` = `sys_`, `cloud_`, `ai_`, which a stack's own objects may not use) is rung 2 of the header's severity ladder: `severity: 'error'`, `object-reference-unknown`. The field site walks `lookup`, `master_detail` and `user` (`RELATIONSHIP_TARGET_FIELD_TYPES`). The rule is a member of `REFERENCE_INTEGRITY_RULES`, which runs on `validate`, `lint` and `build`. - **Build, flow trigger object.** `packages/lint/src/validate-flow-trigger-readiness.ts`: `validateFlowTriggerReadiness` returns `flow-trigger-unknown-object` at `severity: 'warning'` when no `options.packageScope` is stated, with the hint "ignore this if another installed package declares it"; the CLI never states one. Its registry entry in `authoring-rules.ts` carries `commands: ALL`. - **Save door, closure.** `packages/metadata-protocol/src/protocol-authoring-gate.ts` resolves `this.resolveWritePackageScope(evt.packageId)` (the transitive `manifest.dependencies` walk over the live registry, `protocol-write-package-scope.ts`), and `packages/lint/src/runtime-gate.ts` `buildRuntimeWriteSnapshotSet` narrows `objects` with `narrowObjectsToPackageClosure`. Under that closure a flow's trigger object outside it is `flow-trigger-unknown-object` at `error`, a dataset's base object is `object-reference-unknown`, and a list view's object is `list-view-object-outside-closure`; a declared dependency's objects survive the narrowing. - **Save door, lookup.** `validateReferenceIntegrity` skips a member whose `runtimeTypes` does not list the written type, and `validateObjectReferences` declares `runtimeTypes: ['flow', 'dataset']` (`reference-integrity-suite.ts`). So a field `reference` written through an `object` save is not judged there. Read from source, not exercised through a door; it matches the dev report and the director's re-reading on #25805. ## Tests No test changed. The readings above are pinned by existing tests, run at `d1c404fc` after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint`: - `validate-object-references.test.ts`: "NON-DEGENERACY — a name NO package in the artifact provides still errors" (`crm_contract`, the card's cross-repo spelling, is `error` / `object-reference-unknown`). - `runtime-gate.package-closure.test.ts`: "the CLI reading is unchanged: no scope stated, the advisory the rule always gave" (`warning` with no closure, `error` under one), and the flow door's "admits the own package, a declared dependency, platform objects and overlay rows". <details><summary>vitest, 3 files</summary> ``` pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-object-references.test.ts src/runtime-gate.package-closure.test.ts src/validate-flow-trigger-readiness.test.ts Test Files 3 passed (3) Tests 210 passed (210) os-verify-lock: VERDICT command-exit 0 ``` </details> ## Acceptance notes - **Not edited, outside the three sentences the card names.** TL;DR item 2, Context §1 ("Reference is already judged against the declared closure — at the runtime save door, nowhere else"), D1's sentence just before the corrected one ("An undeclared cross-app reference is refused where it is refused today — at the save door, `422`") and Consequences / Positive bullet 2 speak of the save door's closure for every cross-app reference. At `f5dd057587` that door judges a flow's trigger object, a dataset's base object and a list view's object, and not a field lookup (read from source, as above). The corrected D1 says so for its own paragraph; narrowing the other sentences is the seat's and the maintainer's call. carrier: none. - **Context §1's quotation stays.** Its `os validate`, `os build` and `os lint` "judge neither: …" is byte-identical to `LIST_VIEW_OBJECT_OUTSIDE_CLOSURE_EXPLANATION` in `packages/lint/src/rule-explanations.ts`, and it is true there: it names the two list-view verdicts, which the CLI does not run. - **The card's wording, measured.** The card's Expected says the build refuses a cross-package lookup / `record_change` trigger target. The lookup is refused (`error`); the trigger object only warns at build, because no closure is stated there. The text says what the tree does. ## Verification <details><summary>dispatch-gates --commands at d1c404f: 21 families, all exit 0</summary> - `node scripts/check-adr-links.mjs` → exit 0 - `node scripts/check-adr-links.mjs --self-test` → exit 0 - `node scripts/check-closing-keyword-parity.mjs` → exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` → exit 0 - `node scripts/check-comment-mask-corpus.mjs` → exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` → exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` → exit 0 - `pnpm check:adr-anchors` → exit 0 - `pnpm check:adr-index` → exit 0 - `pnpm check:adr-symbol-anchors` → exit 0 - `pnpm check:ci-filter-parity` → exit 0 - `pnpm check:closed-ceremony-text` → exit 0 - `pnpm check:cross-package-test-inputs` → exit 0 - `pnpm check:doc-authoring` → exit 0 - `pnpm check:driver-memory-census` → exit 0 - `pnpm check:gitlink-declared` → exit 0 - `pnpm check:nul-bytes` → exit 0 - `pnpm check:pm-governed-merges` → exit 0 - `pnpm check:pm-prior-rulings` → exit 0 - `pnpm check:refd-timer-probe` → exit 0 - `pnpm check:watch-hint-literal` → exit 0 The first run of `check:doc-formula-expressions` exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` not built); it was re-run after the build and exited 0. ``` ✓ dispatch-gates --ran: 21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED (a DERIVED zero — all 21 recorded an exit code and none of them is 3). ✓ check:commit-card-trailers: 1 commit message(s) on origin/main..HEAD carry no card relation and no model identifier in the trailer pair. ``` </details> Every other family CI runs on this PR is read from its check-runs, not re-run here. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cyxnej16LG3YiCoKe6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6226e7c commit 7b8bcfe

1 file changed

Lines changed: 25 additions & 14 deletions

File tree

‎docs/adr/0141-inter-app-dependency-declared-checked-before-install.md‎

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ HotCRM's module split.
6565
**Evidence**: every anchor into this repository was read at `origin/main` `47af3dde42`, the base of this record's branch
6666
(the tree outside `docs/adr/` is identical at the record's head). `objectstack-ai/hotcrm` and `objectstack-ai/cloud` are
6767
not readable from the drafting session; their facts are cited as the rulings and the HotCRM seat cite them and marked
68-
**not re-verified**.
68+
**not re-verified**. The sentences on where a cross-app reference is judged — TL;DR item 2, D1, D7, the Consequences
69+
and Execution plan items 4 and 5 — were re-read at `f5dd057587` and corrected on 2026-10-30 (#27616).
6970

7071
---
7172

@@ -77,7 +78,9 @@ added; one gate learns a second source of co-owners:
7778
1. **Declaration** is the `dependencies` key every manifest already has — `{ "PACKAGE_ID": "SEMVER_RANGE" }`.
7879
2. **Reference** is what the runtime save door already admits: a package's own objects, the objects of the packages its
7980
`dependencies` declare (transitively), platform objects, and tenant objects that belong to no package. An undeclared
80-
cross-app reference is refused there today, `422`, with "declare a dependency" as the fix.
81+
cross-app reference that door judges — a flow's trigger, a dataset's base, a list view's object — is refused there
82+
today, `422`, with "declare a dependency" as the fix. A lookup is judged only at build, which refuses one into
83+
another artifact whatever the manifest declares (D1).
8184
3. **The one new gate is a pre-install check.** Installing a package judges every declared dependency — installed in the
8285
target environment at a version inside the declared range, or delivered by the same artifact (presence only: the
8386
artifact is one tested unit) — and otherwise refuses the whole install, before anything registers, with a message
@@ -195,13 +198,16 @@ An app — any installable package — declares what it depends on in `ManifestS
195198
platform already admits for a declared package: the runtime save door judges a write against the transitive closure
196199
described in Context §1, and `defineStack` keeps admitting a lookup or master-detail to an object the stack does not
197200
carry while refusing a hook on it and a package's own navigation to it (ADR-0130 §1.5). Nothing else decides whether a
198-
cross-app reference is legal.
201+
cross-app reference is legal, though the build still refuses a declared lookup into another artifact (the gap below).
199202

200203
⛔ No owner-side "published object" flag, no export list, no second reference mechanism (the ruling's own words: the
201-
closure "already honours the declared packages' objects"). An undeclared cross-app reference is refused where it is
202-
refused today — at the save door, `422` — and the fix it prints is the declaration. `os validate`, `os build` and
203-
`os lint` keep judging neither side of a cross-package binding: a stack there is one app and cannot see its dependencies'
204-
objects.
204+
closure "already honours the declared packages' objects"). An undeclared cross-app reference the save door judges is
205+
refused where it is refused today — at the save door, `422` — and the fix it prints is the declaration. `os validate`,
206+
`os build` and `os lint` cannot see the objects of a dependency the artifact does not carry: a stack there resolves
207+
against its own objects, its artifact's `packages[]` and the platform's names, so a lookup to such an object is refused
208+
(`object-reference-unknown`) and a flow triggered on one warns (`flow-trigger-unknown-object`). Only the save door
209+
honours the declaration, for a flow's trigger, a dataset's base and a list view's object; it does not judge a lookup.
210+
Closing that gap at build is deferred by the ruling on #25805.
205211

206212
### D2 — The pre-install check: installed and in range, or a clear refusal — and only that gate
207213

@@ -487,6 +493,8 @@ Three HotCRM-side preconditions, each **not re-verified** and each the HotCRM la
487493

488494
Until the runtime card ships, composite products keep shipping as one artifact under ADR-0130. That is the state
489495
hotcrm#2270 packages revenue and marketing in; hotcrm#2271 moves revenue to its own artifact once the check is released.
496+
Even then, until the build gap D1 names closes (#25805, deferred), a carved-out module's `Field.lookup` into its app's
497+
objects is refused at `os build` whatever the module declares.
490498

491499
### D8 — The dependency shapes, and the alignment the runtime card owes
492500

@@ -532,9 +540,11 @@ one way to declare a dependency.
532540
- A customer installs CRM now and PSA — or a HotCRM module — later, without the module having been shipped to every CRM
533541
customer and without swapping to a combined app: 「每个包可以独立安装」.
534542
- One rule replaces several special cases. Inside an artifact (ADR-0130), at the save door (the closure), and across apps
535-
(this record), a cross-package reference is legal exactly when it is declared.
536-
- An AI author's undeclared reference stays a loud `422` at the save door, and a missing dependency becomes a loud refusal
537-
at install, instead of copied objects that drift silently.
543+
(this record), a cross-package reference is legal exactly when it is declared — save the build's refusal of a lookup
544+
into another artifact, the gap D1 names.
545+
- An AI author's undeclared reference stays a loud `422` at the save door for the bindings that door judges — a lookup
546+
is refused at build instead (D1) — and a missing dependency becomes a loud refusal at install, instead of copied
547+
objects that drift silently.
538548
- The work is small and stays small — one runtime card of size S and two smaller ones: no new authorable key, no schema
539549
change, one predicate at three doors, one more source for one co-owner set (read two ways: a declared parent, a shared
540550
declared parent), one author-side lint rule, and the one sorter applied inside two hydrate loops.
@@ -561,7 +571,7 @@ one way to declare a dependency.
561571
**Unchanged** — the consumer model (ADR-0019 D1–D4: one app installs, opens, uninstalls — a standalone module is itself
562572
an App), the artifact as a control-plane noun that installs whole (ADR-0130 D2), the one sorter (ADR-0130 D5), one
563573
version per artifact (ADR-0130 D6), permission sets (ADR-0086 D4), the save door's closure rules, the snapshot shape
564-
(D8, until its own card), and the compile-time boundary (`os validate` judges neither side of a cross-artifact binding).
574+
(D8, until its own card), and the compile-time boundary (`os validate` refuses a lookup into another artifact, D1).
565575

566576
---
567577

@@ -727,11 +737,12 @@ sizing decision; 10 is a reading of an earlier ruling.
727737
control-plane reader of `sys_package_version.manifest_json`, then the one record-to-array conversion. Independent of
728738
(1); snapshots keep the record shape until it lands.
729739
4. **Docs**: the package-authoring guide states how to declare a dependency, the range grammar, what the install refusal
730-
means, that two modules of one app need not declare each other, and that `os validate` cannot see a dependency's
731-
objects.
740+
means, that two modules of one app need not declare each other, and that `os validate` cannot see the objects of a
741+
dependency the artifact does not carry, so it refuses a lookup to one and warns on a flow triggered on one (D1).
732742
5. **HotCRM** (its own lane): the D4 readings on the revenue module; D7's preconditions — service and marketing declare
733743
sales (Open items 7), hooks follow ownership, revenue's permission sets travel with it — and the carved-out module's
734-
`type: 'app'`; hotcrm#2271 ships revenue as its own artifact once (1) is released.
744+
`type: 'app'`; hotcrm#2271 ships revenue as its own artifact once (1) is released, and until D1's build gap closes
745+
`os build` refuses that module's lookups into HotCRM's objects (D7).
735746

736747
## References
737748

0 commit comments

Comments
 (0)