Skip to content

Commit 7dcbc03

Browse files
fix(cli): os build runs the override write gate os validate runs (#28681)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X6A9L2waqB8UEYtAxtpZGhG)_ Fixes #28643 Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: none — no file under `skills/**` teaches the `overrides` declaration or a `.custom` package; `skills/objectstack-platform/references/operations.md` already says `os validate` and `os build` run the same gates, which this diff makes true for one more gate. ## 维护者速读 - **改了什么**:`os build` 现在会跑 `os validate`、发布接口和托管安装已经在跑的「覆盖声明」判定(同一个 `findOverrideDeclarationRefusals`),命中就报同样的行、退出码 1,不写产物。 - **为什么改**:以前 `os build` 照样产出这种产物,要等到安装或发布时才被同一个判定拒掉;现在在构建时就拒,作者早知道。 - **风险与回滚**:低。只收紧 `os build`:它现在拒的产物,任何一个落地口本来就会拒;干净的配置产物字节不变。回滚即删 `compile.ts` 里新加的那一段。 - **先看**:`packages/cli/src/commands/compile.ts`(新增的 3b-iii 段);`packages/cli/test/build-override-declarations.test.ts`(与 `os validate` 同进程比对);`packages/cli/test/validate-build-gate-parity.test.ts`(台账行从 VALIDATE_ONLY 挪到 SHARED)。 - **两分钟验证**:`cd packages/cli && pnpm exec vitest run --project unit test/build-override-declarations.test.ts`,预期 4/4 通过。 - **需要你做的**:无,席位落地。 ## What changes - `packages/cli/src/commands/compile.ts`: new step 3b-iii, right before the dependency-range check (3b-quater) and ahead of every artifact write. It calls `findOverrideDeclarationRefusals(result.data)`, the same call `os validate` makes at its step 3a-iii, over the same walk (`utils/override-declarations.ts`, reused unchanged) and the publish door's own judge (`judgeOverrideWrite`). On any row it exits 1 and writes no artifact. The `--json` face is this command's failure envelope (`{ success: false, errors, warnings, conversions }`, the shape the view-container refusal at step 3a uses) with the rows `os validate --json` carries on `errors`. The text face prints the same heading and `PATH [CODE] MESSAGE` bullets `os validate` prints. No second spelling of the judge: the messages are the judge's. - `packages/cli/test/validate-build-gate-parity.test.ts`: the `findOverrideDeclarationRefusals` row leaves `VALIDATE_ONLY_GATES` (now empty again, its steady state) and joins `SHARED_NON_REGISTRY_GATES`, where both commands are held to it. The ledger header now names both rows it has held. - `packages/cli/test/build-override-declarations.test.ts` (new, unit tier, in-process): runs `Compile` and `Validate` in the same process (`Command.run`, no CLI spawn) over three `defineStack` configs. (1) A dependent declaring `overrides` of `com.acme.crm` (only `com.acme.crm.custom` may) and (2) a `packages[]` stack whose dependent ships the base's dashboard name undeclared: `os build --json` exits with `os validate`'s exit code, its `errors` deep-equal `os validate --json`'s `errors`, and no artifact exists. (3) The text face prints `os validate`'s refusal block verbatim and not `Build complete`. CONTROL: the base's paired `.custom` declaring the same override builds and validates clean. - `.changeset/28643-build-override-write-gate.md`: `@objectstack/cli` patch, both declaration lines, and an ADR-0087 `not-required (no-migration-prescription)` disposition (the gate asks for one on a `(narrowing)` arm). Not touched: `validate.ts`, `override-declarations.ts`, the judge, every door. This is a product refusal in a CLI command; no repo/CI gate is added. ## Why PR #28554 (#28410) wired the override write gate into the publish doors, `os validate` and the managed install, and honestly recorded `os build` as a `VALIDATE_ONLY_GATES` gap. So `os build` emitted an artifact that the install or a publish door later refused with the same judge: a late refusal, never a false acceptance. Triage `6557795179` answered A: wire the existing judge into the build. ## Tests <details><summary>Red first on origin/main 0b6a52e: os build emits what os validate refuses</summary> Spawned `node packages/cli/bin/run-dev.js` (source entry) in scratch projects with spec linked, before the edit: ``` partner (dependent declares overrides of com.acme.crm) os build --json exit=0 {"success":true,...,"dashboards":1} dist/objectstack.json written undeclared (packages[]: com.acme.partner ships dashboard crm_board, which com.acme.crm in its closure ships) os build --json exit=0 {"success":true,...,"dashboards":2} dist/objectstack.json written os validate --json exit=1 errors=[{path:"packages[1].manifest.dashboards[0]",code:"NOT_OVERRIDABLE",rule:"undeclared_same_name",packageId:"com.acme.partner",...}] pair (com.acme.crm.custom declares the same override) os build --json exit=0 artifact sha256 638c0fb8…b0b3a6 ``` </details> <details><summary>Green with the fix (same scratch projects, same entry)</summary> ``` partner os build --json exit=1 {"success":false,"errors":[{"path":"dashboards[0]","code":"NOT_OVERRIDABLE","rule":"declared_outside_pair","packageId":"com.acme.partner",...}]} no dist/ undeclared os build --json exit=1 errors=[{path:"packages[1].manifest.dashboards[0]",code:"NOT_OVERRIDABLE","rule":"undeclared_same_name",...}] no dist/ undeclared os build (text) exit=1, prints the same heading and bullet as os validate (text): ✗ The publish door would refuse 1 item under the override write gate (ADR-0146 D2) • packages[1].manifest.dashboards[0] [NOT_OVERRIDABLE] 'dashboard/crm_board' in package 'com.acme.partner' bears the name of the dashboard 'com.acme.crm' ships, ... pair os build --json exit=0, artifact sha256 638c0fb8…b0b3a6 — byte-identical to base (control) ``` `os validate` output is unchanged: `validate.ts` is not in the diff. </details> <details><summary>Ablation: removing the call reds the pin, the control stays green</summary> `node scripts/ablation-replace.mjs` on `compile.ts`, anchor `const overrideRefusals = findOverrideDeclarationRefusals(result.data as Record<string, unknown>);` replaced with an empty row list (anchor 1 → 0, replacement 0 → 1, blob `101f6bc6` → `597254ac`). The test imports `src/` directly, so no rebuild is involved. ``` × THE PIN (partner) · × THE PIN (undeclared) · × the text face ... Tests 3 failed | 1 passed (4) ablation-replace: ok restored: blob == HEAD (101f6bc) and `git diff HEAD` is empty ``` </details> <details><summary>Package suites and dogfood</summary> - `pnpm --filter @objectstack/cli typecheck` then `pnpm exec vitest run --project unit --maxWorkers=2` (cli), under the lock at `3ed0c554`: typecheck clean; `Test Files 364 passed (364) · Tests 5167 passed (5167) · VERDICT command-exit 0`. The new file: 4/4, tests 4.6 s (first case 4.3 s is the command graph import). - The `integration` tier is declared to CI: the diff touches no spawn entry and no integration-tier file. - Full dogfood (the `(narrowing)` arm), `@objectstack/dogfood^...` built, then `pnpm --filter @objectstack/dogfood test` under the lock at `3ed0c554`: `Test Files 369 passed | 3 skipped (372) · Tests 2981 passed | 28 skipped (3009) · VERDICT command-exit 0`. Head `9d2ea032` adds only the changeset's ADR-0087 line on top of it. </details> ## Acceptance notes - The text-face heading (`The publish door would refuse N items under the override write gate (ADR-0146 D2)`) is written in `compile.ts` as it is in `validate.ts`, the same way the #20393 view-container refusal is written in both. Folding it into one exported heading (the `picklistRefusalHeading` shape) would edit `validate.ts`, which is outside this claim's file surface. The test pins the build's block EQUAL to `os validate`'s, so the two cannot drift silently. - Placement follows the ruling: beside `checkDependencyRanges`. `os validate` runs the gate a few steps earlier (3a-iii, before its dependency-range step 3e); both refuse before any artifact write, so the accept set is the same. - Overlap: re-read before opening. No open PR touches `compile.ts`, `override-declarations.ts` or the parity test (open PRs 28666, 28662, 28654, 28651, 28650, 28499 checked by file list). ## Verification Derived gate loop (`node scripts/pm/dispatch-gates.mjs --commands`, 61 families) at `3ed0c554`, changeset gates re-run at `9d2ea032`: 60 exit 0, 1 NOT MEASURED. `dispatch-gates --ran`: `61 derived famil(ies) accounted for — 60 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. Record: `pm-orders/server/28643-gates-ran.txt`. - NOT MEASURED: `check:dual-build-cjs-loads`, reason: needs every package's `dist/` (a full `pnpm build`), here `packages/apps/studio/dist` was absent; CI runs it. - `check:type-check-debt` and `check:i18n-coverage` first read exit 3 (lock queue-timeout behind the dogfood hold; showcase deps unbuilt) and were re-run under the lock: `check-type-check-coverage --re-measure: OK — 0 raw tsc error(s)` and `check-i18n-coverage: OK (14 config(s), 527 baselined untranslated string(s), none new)`. - `check-adr-0087-registration` read exit 1 before the disposition line was added, exit 0 after. - CI owns the repo-wide lint and every family outside the derived set. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5nEQqaP6EhMwUoWpZf --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5nEQqaP6EhMwUoWpZf)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 49a6a5c commit 7dcbc03

4 files changed

Lines changed: 230 additions & 15 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os build` runs the override write gate `os validate` runs (ADR-0146 D2, #28643)
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
`os build` now calls `findOverrideDeclarationRefusals` beside the dependency-range check, the same walk over the publish door's own judge (`judgeOverrideWrite`) that `os validate`, the publish doors and the managed install ask. A stack carrying an `overrides` declaration the gate does not admit, or a dependent's item bearing a name a package in its dependency closure ships, is refused at build with the rows `os validate` prints (`NOT_OVERRIDABLE`, exit 1), and no artifact is written. Before, the build emitted that artifact and the managed install or a publish door refused it later with the same judge; what any door accepts is unchanged.
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: `os build` now runs the override write gate `os validate`, the publish doors and the managed install already run, with the same judge (`judgeOverrideWrite`) and the same rows. An artifact it now refuses was already refused wherever it landed, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone; the remedy is the judge's own sentence (rename the item, or remove `overrides`). -->

‎packages/cli/src/commands/compile.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ import { findViewContainerNameRefusals } from '../utils/view-container-names.js'
7676
// A field `picklist` that names no picklist the stack declares — the walk
7777
// `os validate` step 2d runs, over the same judge.
7878
import { judgePicklistReferences, picklistRefusalHeading, printPicklistReferenceNotices } from '../utils/picklist-references.js';
79+
// [ADR-0146 D2, #28643] The override write gate — the walk `os validate` step
80+
// 3a-iii runs, over the publish door's own judge (`judgeOverrideWrite`).
81+
import { findOverrideDeclarationRefusals } from '../utils/override-declarations.js';
7982

8083
export default class Compile extends Command {
8184
static override description = 'Compile ObjectStack configuration to JSON artifact';
@@ -768,6 +771,37 @@ export default class Compile extends Command {
768771
);
769772
}
770773

774+
// 3b-iii. [ADR-0146 D2, #28643] The override write gate, at build time —
775+
// the SAME call `os validate` makes at its step 3a-iii, over the
776+
// walk `utils/override-declarations.ts` takes and the publish door's
777+
// own judge (`judgeOverrideWrite`). Only the base's paired
778+
// `<base>.custom` declares an override; a dependent's item bearing a
779+
// name a package in its closure ships, and an `overrides` declaration
780+
// the gate does not admit, are refused. Before this the build emitted
781+
// such an artifact and the managed install or a publish door refused
782+
// it later with the same judge; now the door that ships refuses it
783+
// and writes no artifact.
784+
//
785+
// Beside the dependency-range check below and ahead of every artifact
786+
// write. The `--json` face is this command's failure envelope
787+
// (`errors`, the rows `os validate --json` carries); the text face is
788+
// `os validate`'s. No step line, as on `os validate`.
789+
const overrideRefusals = findOverrideDeclarationRefusals(result.data as Record<string, unknown>);
790+
if (overrideRefusals.length > 0) {
791+
if (flags.json) {
792+
await emitJson({ success: false, errors: overrideRefusals, warnings: warningsSoFar(), conversions: conversionNotices }, 0, { compact: true });
793+
this.exit(1);
794+
}
795+
const n = overrideRefusals.length;
796+
console.log('');
797+
printError(`The publish door would refuse ${n} item${n > 1 ? 's' : ''} under the override write gate (ADR-0146 D2)`);
798+
printBulletList(
799+
overrideRefusals.map((r) => `${r.path} [${r.code}] ${r.message}`),
800+
{ noun: 'override refusal(s)', remedy: JSON_FULL_LIST_REMEDY },
801+
);
802+
this.exit(1);
803+
}
804+
771805
// 3b-quater. ADR-0141 D2, author side — every `manifest.dependencies`
772806
// range of the packages one install act would deliver, judged by the
773807
// install's own predicate (`utils/dependency-range-check.ts`).
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [ADR-0146 D2, #28643] `os build` asks the override write gate that `os
5+
* validate`, the publish doors and the managed install ask, and refuses the
6+
* build with the rows `os validate` prints: same rows, same exit code, and
7+
* NO artifact written.
8+
*
9+
* ## The defect, measured before the fix
10+
*
11+
* On a dependent package carrying `overrides: { package: 'com.acme.crm', … }`
12+
* (only `com.acme.crm.custom` may declare one), and on a `packages[]` stack
13+
* whose dependent ships a dashboard under the base's name, `os build --json`
14+
* exited 0 and wrote `dist/objectstack.json`, while `os validate --json`
15+
* exited 1 with one `NOT_OVERRIDABLE` row each. The managed install and the
16+
* publish doors refused that artifact later with the same judge.
17+
*
18+
* ## One judge, one walk
19+
*
20+
* `compile.ts` makes the call `validate.ts` makes
21+
* (`findOverrideDeclarationRefusals`, over `judgeOverrideWrite`), so the pins
22+
* below assert EQUALITY with what `os validate` answers for the same config,
23+
* run in the same process, rather than a copy of the judge's words. Both
24+
* commands run in-process (`Command.run`), not as spawned CLIs.
25+
*/
26+
27+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
28+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
29+
import { tmpdir } from 'node:os';
30+
import { join, resolve } from 'node:path';
31+
import { fileURLToPath } from 'node:url';
32+
import Compile from '../src/commands/compile';
33+
import Validate from '../src/commands/validate';
34+
import { defineStackSource, linkSpec } from './helpers/define-stack-fixture.js';
35+
36+
const CLI_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..');
37+
const BASE = 'com.acme.crm';
38+
39+
interface Run {
40+
/** The exit code `this.exit(n)` threw, or 0 when the command returned. */
41+
code: number;
42+
stdout: string;
43+
}
44+
45+
/** Runs one command in-process; stdout captured ANSI-free, the write callback honoured. */
46+
async function run(command: typeof Compile | typeof Validate, argv: string[]): Promise<Run> {
47+
const chunks: string[] = [];
48+
const spy = vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: unknown, ...rest: unknown[]) => {
49+
chunks.push(String(chunk));
50+
const done = rest.find((a) => typeof a === 'function') as ((e?: Error | null) => void) | undefined;
51+
done?.(null);
52+
return true;
53+
}) as never);
54+
const exitCodeBefore = process.exitCode;
55+
let code = 0;
56+
try {
57+
await command.run(argv, { root: CLI_ROOT });
58+
} catch (error) {
59+
const exit = (error as { oclif?: { exit?: unknown } }).oclif?.exit;
60+
if (typeof exit !== 'number') throw error;
61+
code = exit;
62+
} finally {
63+
spy.mockRestore();
64+
process.exitCode = exitCodeBefore;
65+
}
66+
// eslint-disable-next-line no-control-regex
67+
return { code, stdout: chunks.join('').replace(/\u001b\[[0-9;]*m/g, '') };
68+
}
69+
70+
const json = (r: Run, label: string): Record<string, any> => {
71+
try {
72+
return JSON.parse(r.stdout) as Record<string, any>;
73+
} catch {
74+
throw new Error(`${label}: stdout was not one JSON document (exit ${r.code})\n${r.stdout}`);
75+
}
76+
};
77+
78+
const dashboard = (name: string, extra: Record<string, unknown> = {}) => ({ name, label: 'Board', widgets: [], ...extra });
79+
80+
/** A single-package stack declaring an override of the base's dashboard. */
81+
const declaring = (id: string): Record<string, unknown> => ({
82+
manifest: { id, name: 'ovr', version: '1.0.0', type: 'app', namespace: 'ovr', dependencies: { [BASE]: '^1.0.0' } },
83+
dashboards: [dashboard('crm_board', { overrides: { package: BASE, basedOn: '1.0.0' } })],
84+
});
85+
86+
/** A `packages[]` stack whose dependent ships the base's dashboard name, undeclared. */
87+
const undeclaredSameName = (): Record<string, unknown> => ({
88+
manifest: { id: 'com.acme.bundle', name: 'bundle', version: '1.0.0', type: 'app' },
89+
packages: [
90+
{ manifest: { id: BASE, name: 'crm', version: '1.0.0', type: 'app', dashboards: [dashboard('crm_board')] } },
91+
{
92+
manifest: {
93+
id: 'com.acme.partner', name: 'partner', version: '1.0.0', type: 'app',
94+
dependencies: { [BASE]: '^1.0.0' }, dashboards: [dashboard('crm_board')],
95+
},
96+
},
97+
],
98+
});
99+
100+
let root = '';
101+
const fixtures: Record<string, { config: string; artifact: string }> = {};
102+
103+
beforeAll(() => {
104+
root = mkdtempSync(join(tmpdir(), 'os-build-override-declarations-'));
105+
const make = (label: string, s: Record<string, unknown>) => {
106+
const dir = join(root, label);
107+
mkdirSync(dir, { recursive: true });
108+
// `strict: false`: the content is the door's to judge, not the producer's.
109+
writeFileSync(join(dir, 'objectstack.config.ts'), defineStackSource(s, { strict: false }));
110+
linkSpec(dir);
111+
fixtures[label] = { config: join(dir, 'objectstack.config.ts'), artifact: join(dir, 'dist', 'objectstack.json') };
112+
};
113+
make('partner', declaring('com.acme.partner'));
114+
make('undeclared', undeclaredSameName());
115+
make('pair', declaring('com.acme.crm.custom'));
116+
});
117+
118+
afterAll(() => {
119+
if (root) rmSync(root, { recursive: true, force: true });
120+
});
121+
122+
const build = (label: string, ...flags: string[]) =>
123+
run(Compile, [fixtures[label].config, '--output', fixtures[label].artifact, ...flags]);
124+
const validate = (label: string, ...flags: string[]) => run(Validate, [fixtures[label].config, ...flags]);
125+
126+
describe('#28643 — os build refuses what os validate refuses under the override write gate, and ships nothing', () => {
127+
it.each(['partner', 'undeclared'])('THE PIN (%s): --json exits as validate does, with validate\'s rows, and writes no artifact', async (label) => {
128+
const validated = await validate(label, '--json');
129+
expect(validated.code, validated.stdout).toBe(1);
130+
const expected = json(validated, `validate ${label}`).errors as Array<Record<string, unknown>>;
131+
// Anti-vacuity: the judge refuses this config, under the override gate.
132+
expect(expected.map((e) => e.code)).toEqual(['NOT_OVERRIDABLE']);
133+
134+
const built = await build(label, '--json');
135+
const payload = json(built, `build ${label}`);
136+
expect(built.code).toBe(validated.code);
137+
expect(payload.success).toBe(false);
138+
expect(payload.errors).toEqual(expected);
139+
expect(existsSync(fixtures[label].artifact), 'os build wrote an artifact the publish door refuses').toBe(false);
140+
}, 60_000);
141+
142+
it('the text face exits 1 and prints the refusal block os validate prints', async () => {
143+
const validated = await validate('undeclared');
144+
expect(validated.code).toBe(1);
145+
const block = validated.stdout.slice(validated.stdout.indexOf('✗ The publish door would refuse')).trimEnd();
146+
expect(block).toContain('[NOT_OVERRIDABLE]');
147+
148+
const built = await build('undeclared');
149+
expect(built.code).toBe(1);
150+
expect(built.stdout).toContain(block);
151+
expect(built.stdout).not.toContain('Build complete');
152+
expect(existsSync(fixtures.undeclared.artifact)).toBe(false);
153+
}, 60_000);
154+
155+
it('CONTROL: the base\'s paired .custom declaring the same override builds, and validates clean', async () => {
156+
expect((await validate('pair', '--json')).code).toBe(0);
157+
const built = await build('pair', '--json');
158+
const payload = json(built, 'build pair');
159+
expect(payload.success, JSON.stringify(payload.errors ?? payload.error)).toBe(true);
160+
expect(built.code).toBe(0);
161+
const artifact = JSON.parse(readFileSync(fixtures.pair.artifact, 'utf8')) as Record<string, any>;
162+
expect(artifact.dashboards.map((d: { name: string }) => d.name)).toEqual(['crm_board']);
163+
}, 60_000);
164+
});

‎packages/cli/test/validate-build-gate-parity.test.ts‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,20 @@ const SHARED_NON_REGISTRY_GATES: readonly string[] = [
183183
// and the walk reads every dataset where it sits, the package bodies
184184
// included, across the whole artifact.
185185
'judgeSeedRecordValues',
186+
// [ADR-0146 D2, #28410, #28643] The override write gate: an item bearing a
187+
// name a package in its dependency closure ships, or an `overrides`
188+
// declaration the gate does not admit, judged by the publish door's own
189+
// `judgeOverrideWrite`. Not a registry rule: the WALK decides which packages
190+
// of the artifact are in each package's closure and which of them ship the
191+
// same name, so it is cross-package by construction.
192+
//
193+
// ⭐ This row is the #28410 VALIDATE_ONLY_GATES entry CLOSED. That entry read
194+
// "os build is a fourth point it did not name", and it was right: the build
195+
// emitted an artifact the managed install and the publish doors refused
196+
// later. `compile.ts` now makes the same call (#28643), so the row moved
197+
// here — deleted there rather than reworded, for the reason the
198+
// `runPerPackageAuthoringRules` row above gives.
199+
'findOverrideDeclarationRefusals',
186200
];
187201

188202
/**
@@ -221,22 +235,13 @@ const BUILD_ONLY_GATES: Readonly<Record<string, string>> = {
221235
* SHARED_NON_REGISTRY_GATES instead.
222236
*
223237
* EMPTY is this ledger's steady state: every row is a gap the build carries.
224-
* Its one row, `findViewContainerNameRefusals` (#20331), moved to
225-
* SHARED_NON_REGISTRY_GATES when `compile.ts` gained the call (#20393). The
226-
* ledger stays, empty, because it is the only honest place the closed roster
227-
* has for the next validate-only gate.
238+
* Both rows it has held moved to SHARED_NON_REGISTRY_GATES when `compile.ts`
239+
* gained the call: `findViewContainerNameRefusals` (#20331 → #20393) and
240+
* `findOverrideDeclarationRefusals` (#28410 → #28643). The ledger stays,
241+
* empty, because it is the only honest place the closed roster has for the
242+
* next validate-only gate.
228243
*/
229-
const VALIDATE_ONLY_GATES: Readonly<Record<string, string>> = {
230-
// [#28410] A real gap in the build's direction, reported and not closed:
231-
// card #28410 (ADR-0146 D2, ruling A on #28390) named three gate points for
232-
// the override write gate — the publish doors, `os validate` and the
233-
// install door — and `os build` is not one of them. The artifact a build
234-
// emits is still judged where it lands: the marketplace managed install
235-
// and the publish doors refuse the same items with the same judge. Whether
236-
// the build refuses too is a separate decision, raised on the card.
237-
findOverrideDeclarationRefusals:
238-
'card #28410 placed the override write gate at publish, os validate and install; os build is a fourth point it did not name (open question on #28410). The install and publish doors refuse the same items with the same judge.',
239-
};
244+
const VALIDATE_ONLY_GATES: Readonly<Record<string, string>> = {};
240245

241246
/**
242247
* Everything else the two commands call, and the reason each one is NOT an

0 commit comments

Comments
 (0)