Skip to content

Commit bc2ec80

Browse files
feat(devx): extend AMPLIFIERS to the packages a sibling checkout links (#18749)
Part of #16529 — this lands the objectstack half of the maintainer's ruling (batch #151 item 1, letter **C**). It is deliberately NOT a closing reference: the ruling's scope is "cloud's 184-specifier closure (`plugin-auth`, `core`, `organizations` **and the rest**)", and "the rest" is not derivable inside this container (see *Declared gap* below). The card should be closed by hand once that gap is judged, and the cloud half (the sibling preflight reading the stamp) is a separate cloud card `Blocked-by:` this one, per the ruling's Execution section. ## What this changes `scripts/check-dev-prereqs.mjs`'s `AMPLIFIERS` list gains three packages, and each of their build scripts gains `--stamp` as the last `&&`-joined step: | package | path | build script now ends | |---|---|---| | `@objectstack/core` | `packages/core` | `tsup && node ../../scripts/check-dts-emitted.mjs && node ../../scripts/check-dev-prereqs.mjs --stamp` | | `@objectstack/plugin-auth` | `packages/plugins/plugin-auth` | `tsup && node ../../../scripts/check-dts-emitted.mjs && node ../../../scripts/check-dev-prereqs.mjs --stamp` | | `@objectstack/organizations` | `packages/plugins/organizations` | `tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs && node ../../../scripts/check-dev-prereqs.mjs --stamp` | Each package was located by the `name` field in its manifest, not by assuming the ruling's word was a path — `organizations` lives one directory deeper than the other two, under `packages/plugins/`. The header gains the second admission criterion (a sibling checkout links it), the ruling's explicit refusal to make the stamp a cross-repo contract, and the declared shortfall. No version number, no stable-location promise, no docs page — the format stays internal dev tooling shared by sibling checkouts, and when it changes the sibling's preflight reds once and is fixed in the same breath. ## Why the stamp and not a source SHA Carried forward from triage at the triage seat's explicit request, so the next reader does not re-propose it: "stamp the source SHA into the build output" sounds more precise and answers **where HEAD is** — which is exactly the half the consumer already does, and exactly the half its own docblock records as insufficient. A sibling sitting *exactly on the pin* whose `dist/` was built from an older commit produces the identical type error, and a HEAD comparison is silent all the way through it. A more precise answer to the wrong question. ## Where `--stamp` goes, and the proof it is right `--stamp` goes **after** `check-dts-emitted.mjs`, matching the existing `packages/spec` entry. The authority is the file's own `isStampReachableOnlyFromCompleteBuild` work — `stampStepOrderProblem` — which rejects three lying spellings. This is machine-checked rather than argued: the coverage gate was driven to **fire on five broken placements and pass on the shipped one**, on the real manifests, with the mutation proven on disk each time and restored from `HEAD` under a trap. Firing leg, mutating `packages/core/package.json` in place (gate exit **1** on every row, each naming `packages/core` — a package this gate could not judge at all before this diff): | injected spelling | gate's diagnosis | |---|---| | `tsup ; node …--stamp` | the step before it is joined by `;`, not `&&` — so the stamp is written even when that step failed | | `tsup \|\| node …--stamp` | the step before it is joined by `\|\|`, not `&&` — so the stamp is written even when that step failed | | `node …--stamp && tsup` | `check-dev-prereqs.mjs --stamp` is not its LAST step | | `tsup && node …--stamp && node …check-dts-emitted.mjs` | `check-dev-prereqs.mjs --stamp` is not its LAST step | | `tsup && node …check-dts-emitted.mjs` (stamp dropped) | `check-dev-prereqs.mjs --stamp` does not appear in it at all | Green leg, on the shipped spelling over a fully built tree (`pnpm build`, 73/73 tasks successful): ``` ✓ 68 package build artifacts present (existence, not freshness). ✓ @objectstack/spec, @objectstack/core, @objectstack/plugin-auth, @objectstack/organizations built from the sources on disk — the only freshness claim this line makes; everything else above is existence only. ``` The three stamps were written by the real builds, not by hand — from the build log: ``` @objectstack/core:build: ✓ packages/core/dist/.build-input-hash ← 134aa01ce7f7368e… @objectstack/plugin-auth:build: ✓ packages/plugins/plugin-auth/dist/.build-input-hash ← 90bd5c9bb3ac8a7d… @objectstack/organizations:build: ✓ packages/plugins/organizations/dist/.build-input-hash ← e8c1627e65820b37… ``` Every mutation leg restored from `HEAD` under a trap with absolute paths, verified by blob hash equality and an empty `git diff HEAD`; the working tree read 0 changed files afterwards. ## The gate now catches the defect the card is about A source edit in a newly listed package that no build has consumed — the shape that surfaces in the sibling as a type error naming an import nobody touched — now reads **stale** instead of passing unexamined: ``` ✗ A built package's dist no longer matches its sources — 1 unmet precondition, not a list of problems. All 68 declared build artifacts are present. They are not all current: @objectstack/core (packages/core/dist/.build-input-hash) built from sources hashing 134aa01ce7f7368e… the sources on disk hash f274704eb5548273… ``` Before this diff that verdict was unreachable for `packages/core`: freshness is only computed for listed packages. ## Declared gap — "and the rest" is not derivable here The ruling starts from cloud's 184-specifier link closure. That closure cannot be read in this container; attaching `objectstack-ai/cloud` to the session was attempted and refused (no access for this session's credential). So this PR delivers **exactly the three packages the ruling names**, and the remainder is reported rather than invented. ⛔ "Every workspace package that emits a `dist/`" was explicitly **not** substituted for that closure — it is a different set, and listing packages nobody links buys a build step for no reader. The shortfall is monotone-safe, and that is the reason it is acceptable to land short: the coverage error fires only on **listed** packages, so a shorter list checks less. It cannot false-red and it cannot turn today's green red. ## Acceptance notes - **Negative controls, both directions, unchanged.** `--stamp` from an unlisted package still exits 1 with its own wording (`packages/metadata is not a declared freshness amplifier…`); a listed package whose build stops stamping still throws a coverage error (row 5 of the firing table). - **No mtime criterion is introduced anywhere** — the family's shared criterion (content, not mtime) holds. The added entries are judged by the same `buildInputHash` content digest the one existing entry is. - **The first segment's protection extends to the new entries for free.** With `packages/plugins/plugin-auth/dist/index.mjs` moved aside, `--stamp` from that package exits 1 (`…is not on disk, so this run emitted nothing for a stamp to vouch for`) instead of stamping over a dist with no build in it. Restored byte-identical (sha256 equal before and after). - **The "two lines" promise re-measured, with one honest correction.** The mechanism is still two lines per package — path in `AMPLIFIERS`, `--stamp` at the end of the build — and nothing else is wired anywhere (no CI step, no turbo entry, no ignore rule needed; `dist/**` is already a turbo output so the stamp caches and cleans with the artifact it describes). What the promise does not cover is the header's *stated admission criterion*: it named exactly one dist, so admitting three more needed the criterion itself written down. That is a documentation edit, not a third wiring step. - **Changeset: measured, not assumed.** `npm pack --dry-run --json` on `@objectstack/core`: **18** tarball entries with the stamps present, **16** with them moved out of the packed tree — the delta is exactly `dist/.build-input-hash` and `dist/.build-input-hash-dts`, which the new last build step writes, and `dist` is in this package's `files[]`. Positive control: `dist/index.js` present in both listings. So something published does move, and a `patch` changeset is owed for the three packages rather than `skip-changeset`. Nothing is imported, executed or resolved from the two files; no export moves. ## Not addressed here `#16529` remains open for the closure gap above. The cloud-side half — the sibling preflight reading this stamp — is not in this repository and is not touched by this PR. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa910a6 commit bc2ec80

5 files changed

Lines changed: 61 additions & 7 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/core': patch
3+
'@objectstack/plugin-auth': patch
4+
'@objectstack/organizations': patch
5+
---
6+
7+
Build freshness: these three packages now write the repo's build-input content
8+
stamp as the last step of their own build, and are checked for freshness (not
9+
merely existence) by `check:dev-prereqs`.
10+
11+
What changes for a consumer: each tarball now carries two extra inert metadata
12+
files inside `dist/` — `.build-input-hash` and `.build-input-hash-dts`, the same
13+
pair `@objectstack/spec` has always shipped. Nothing is imported, executed or
14+
resolved from them, no export moves and no runtime behaviour changes.
15+
16+
Why: a sibling checkout that links these packages by `link:` compiles against
17+
their `dist/`, so a dist built from an older tree surfaces as a type error
18+
naming an import nobody touched, with the symbol present in `src/` the whole
19+
time. A HEAD-versus-pin comparison is silent through that; a content stamp
20+
written by the build itself is not.

‎packages/core/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
}
2020
},
2121
"scripts": {
22-
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
22+
"build": "tsup && node ../../scripts/check-dts-emitted.mjs && node ../../scripts/check-dev-prereqs.mjs --stamp",
2323
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
2424
"check:test-typecheck": "tsx ../../scripts/check-test-typecheck.mts --self-test && tsx ../../scripts/check-test-typecheck.mts --package packages/core --project tsconfig.test.json",
2525
"gen:test-typecheck-debt": "tsx ../../scripts/check-test-typecheck.mts --update --package packages/core --project tsconfig.test.json",

‎packages/plugins/organizations/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
}
1515
},
1616
"scripts": {
17-
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
17+
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs && node ../../../scripts/check-dev-prereqs.mjs --stamp",
1818
"test": "vitest run",
1919
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
2020
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/plugins/organizations --project tsconfig.test.json"

‎packages/plugins/plugin-auth/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
}
2020
},
2121
"scripts": {
22-
"build": "tsup && node ../../../scripts/check-dts-emitted.mjs",
22+
"build": "tsup && node ../../../scripts/check-dts-emitted.mjs && node ../../../scripts/check-dev-prereqs.mjs --stamp",
2323
"test": "vitest run",
2424
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.examples.json && pnpm check:test-typecheck",
2525
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/plugins/plugin-auth --project tsconfig.test.json",

‎scripts/check-dev-prereqs.mjs‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,31 @@
202202
* how every package builds, for packages whose stale dist fails loudly instead
203203
* of lying. AGENTS.md §9's stale-artefact table names exactly one dist that
204204
* presents as *other people's* contract drift, and it is `packages/spec`.
205+
*
206+
* A SECOND ADMISSION CRITERION, read from one repo further out (#16529). A
207+
* sibling checkout links `@objectstack/*` by `link:` — objectstack-ai/cloud
208+
* does it for 184 specifiers — and compiles against the linked package's
209+
* `dist/`. A stale dist there surfaces as `TS2305 … has no exported member
210+
* …` naming an import nobody touched, with the symbol present in `src/` the
211+
* whole time: row one's lie, read across a repository boundary. The consumer
212+
* cannot close it from its side — its preflight compares the sibling's HEAD
213+
* against its pin, and a sibling sitting EXACTLY on the pin whose dist was
214+
* built from an older commit is silent through that comparison. So a package a
215+
* sibling checkout actually links is admitted here too, and the stamp its build
216+
* already writes is what that preflight reads.
217+
* ⛔ That reading does NOT make this file's stamp a cross-repo contract. It is
218+
* internal dev tooling shared by sibling checkouts — both repositories are
219+
* ours, the format may change without notice, and when it does the sibling's
220+
* preflight reds once and is fixed in the same breath. No version number, no
221+
* stable-location promise, no docs page (maintainer ruling on #16529).
222+
* ⚠ THIS LIST IS SHORT OF THAT CLOSURE, declared rather than inherited: the
223+
* three entries under criterion 2 are the ones that ruling names; the remainder
224+
* of the 184-specifier closure is not derivable from inside this repository.
225+
* ⛔ "Every workspace package that emits a `dist/`" is a DIFFERENT set and not
226+
* a stand-in for it — it would add a build step for packages no reader links.
227+
* The shortfall is monotone-safe: the coverage error fires only on LISTED
228+
* packages, so a short list checks less and can never false-red.
229+
*
205230
* Adding the next amplifier is two lines: its path in AMPLIFIERS, and `--stamp`
206231
* at the end of its build script — and NEITHER half can be forgotten, because
207232
* a listed package whose build script does not stamp fails this gate as a
@@ -355,14 +380,23 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
355380

356381
/**
357382
* Packages whose dist is checked for FRESHNESS and not merely existence, as
358-
* workspace-relative POSIX paths. See the header for the admission criterion
359-
* ("a stale dist that presents as somebody else's contract drift") and for why
360-
* this is a declared list rather than every package.
383+
* workspace-relative POSIX paths. See the header for the TWO admission criteria
384+
* ("a stale dist that presents as somebody else's contract drift", and "a
385+
* sibling checkout links it") and for why this is a declared list rather than
386+
* every package.
361387
*
362388
* Every entry MUST end its `build` script with STAMP_INVOCATION; a listed
363389
* package that does not is a coverage error, not a silent pass.
364390
*/
365-
const AMPLIFIERS = ['packages/spec'];
391+
const AMPLIFIERS = [
392+
// Criterion 1 — a stale dist here reads as somebody else's contract drift.
393+
'packages/spec',
394+
// Criterion 2 — linked by `link:` from a sibling checkout, where a stale dist
395+
// is a TS2305 naming an import nobody touched. See the header.
396+
'packages/core',
397+
'packages/plugins/plugin-auth',
398+
'packages/plugins/organizations',
399+
];
366400

367401
/** What an amplifier's build script must END WITH for its stamp to be maintained. */
368402
const STAMP_INVOCATION = 'check-dev-prereqs.mjs --stamp';

0 commit comments

Comments
 (0)