Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/spec/scripts/build-api-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ const CHECK = process.argv.includes('--check');
// `ts.createProgram` has run over a stale dist, every answer below it is
// confidently wrong, and both writing it and checking against it are worse than
// stopping here.
const freshness = inspectDistFreshness(PKG_DIR, CHECK ? 'check' : 'generate');
const freshness = inspectDistFreshness(
PKG_DIR,
CHECK ? 'check' : 'generate',
`pnpm --filter @objectstack/spec ${CHECK ? 'check' : 'gen'}:api-surface`,
);
if (!freshness.fresh) {
console.error(freshness.message);
process.exit(1);
Expand Down
29 changes: 29 additions & 0 deletions packages/spec/scripts/check-dual-source-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,24 @@
* chunk, so distinct dist declarations imply distinct source declarations; the
* self-test pins the detector itself, and the count assertions keep a silent
* resolution failure from reading as "clean".
*
* That precondition is enforced since #7181, and `--update` is why it matters
* here more than in this file's two sibling gates. #7181 was filed on the reading
* that all three "are check-only — none writes a tracked artifact, so none can
* launder a wrong baseline into a commit". This one does: `--update` REWRITES
* `dual-source-exports.baseline.json`, and on a stale dist it writes a partition
* computed from declarations that predate the edit. The ratchet then makes that
* self-consistent in both directions — a name that only became dual-source after
* the last build is written out as clean, and the plain run compares the same
* baseline against the same stale dist and agrees. That is #7122's laundering
* shape exactly, one artifact over. See lib/dist-freshness.ts.
*/
import ts from 'typescript';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { inspectDistFreshness } from './lib/dist-freshness';

const PKG_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');
const BASELINE_PATH = resolve(PKG_DIR, 'dual-source-exports.baseline.json');
Expand Down Expand Up @@ -218,6 +230,23 @@ if (SELF_TEST) selfTest();

// ── Audit ────────────────────────────────────────────────────────────────────

// BEFORE a single `.d.ts` is read (#7181, adopting #7122's primitive). `--update`
// is `generate`-shaped — it writes a tracked baseline — so it gets the writing
// damage, and the plain run gets the false-green one. Placed after `--self-test`
// on purpose: that path builds its own fixture in a temp dir and never reads
// `dist/`, so refusing it on a stale dist would refuse a run that is unaffected.
const freshness = inspectDistFreshness(
PKG_DIR,
UPDATE ? 'generate' : 'check',
UPDATE
? 'pnpm --filter @objectstack/spec exec tsx scripts/check-dual-source-exports.ts --update'
: 'pnpm --filter @objectstack/spec check:dual-source-exports',
);
if (!freshness.fresh) {
console.error(freshness.message);
process.exit(1);
}

const entries = collectEntries();
const { findings, names, reExports } = scan(makeProgram(Object.values(entries)), entries);

Expand Down
28 changes: 28 additions & 0 deletions packages/spec/scripts/check-exported-any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,21 @@
* whose green result is "nothing found".
*
* Reads the built dist — run after `pnpm --filter @objectstack/spec build`.
*
* That last sentence is a PRECONDITION, and since #7181 it is enforced rather
* than merely documented: the audit refuses a dist that is missing or older than
* `src/`. The self-test above is the anti-vacuity floor for a broken DETECTOR; it
* says nothing about the vintage of the declarations the audit then reads, and on
* a stale dist this gate reports "no exported type resolves to `any`" without
* having read the export the developer just added. See lib/dist-freshness.ts.
*/
import ts from 'typescript';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { inspectDistFreshness } from './lib/dist-freshness';

const PKG_DIR = resolve(fileURLToPath(new URL('.', import.meta.url)), '..');
const SELF_TEST = process.argv.includes('--self-test');
Expand Down Expand Up @@ -266,6 +274,26 @@ if (SELF_TEST) selfTest();

// ── Audit ────────────────────────────────────────────────────────────────────

// BEFORE a single `.d.ts` is read (#7181, adopting #7122's primitive). The
// existing floors — the self-test's count assertions, and the "Could not resolve
// module symbol … Is the package built?" throw in `scan` — cover a MISSING dist
// and a broken detector. Neither can see the case this refuses: a dist that is
// present and resolves fine but predates the edit under test. There the audit
// runs to completion and prints `✅ no exported type resolves to \`any\`` about a
// build nobody made, which is a false green on exactly the export the developer
// just wrote. It sits after `--self-test` deliberately: that path compiles a temp
// fixture against the real zod and never touches `dist/`, so refusing it on a
// stale dist would be over-reach.
const freshness = inspectDistFreshness(
PKG_DIR,
'check',
'pnpm --filter @objectstack/spec check:exported-any',
);
if (!freshness.fresh) {
console.error(freshness.message);
process.exit(1);
}

const entries = collectEntries();
const { violations, declared, types, schemas } = scan(makeProgram(Object.values(entries)), entries, KNOWN_ANY);

Expand Down
36 changes: 36 additions & 0 deletions packages/spec/scripts/check-skill-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
* step in CI — alongside `check:api-surface` / the example-app typecheck, its
* fellow "real consumer" gates — not before it like `check:skill-refs`.
*
* Since #7181 that ordering is enforced rather than assumed: the type-check half
* refuses a dist that is missing or older than `src/`. The existing "is the spec
* built" guard below only answers ABSENCE; a present-but-stale dist type-checks
* every example against the previous build and prints `✅ N prose examples
* type-check against @objectstack/spec` — a green about a rename the developer
* has already made and this run never saw. See lib/dist-freshness.ts.
*
* ── The third anti-idle assertion: no bare `any` in a marked block (#5943) ───
* A marker is the author's claim "this block compiles", and the two guards above
* (orphan marker, zero blocks) exist because a gate that checks nothing must not
Expand Down Expand Up @@ -101,6 +108,8 @@ import os from 'os';
import path from 'path';
import ts from 'typescript';

import { inspectDistFreshness } from './lib/dist-freshness';

// ── Paths ────────────────────────────────────────────────────────────────────

const REPO_ROOT = path.resolve(__dirname, '../../..');
Expand Down Expand Up @@ -676,6 +685,33 @@ function main() {
);
}

// BEFORE any declaration is resolved (#7181, adopting #7122's primitive).
//
// Placement differs from the two sibling gates on purpose, because the ROUTE to
// the dist differs: those resolve entry points in-process with
// `ts.createProgram`, so their first `.d.ts` read is their first statement of
// work. Here the declarations are reached indirectly — `specPaths()` turns the
// exports map into a tsconfig `paths` table and a spawned `tsc` follows it — and
// everything above this line (extraction, the orphan-marker guard, the zero-block
// guard, the bare-`any` guard) is dist-independent and worth reporting even when
// the build is stale. So the guard sits at the boundary rather than at the top:
// no verdict below it is computed, and no honest finding above it is suppressed.
//
// The `missing` check immediately following is NOT redundant. It answers "was
// the package built at all", which this also covers via `state: 'missing'`; but
// it stays because it is the one that survives a PARTIAL dist — a subpath whose
// `.d.ts` was never emitted while the newest declaration on disk is still newer
// than `src/`, which the mtime rule reads as fresh.
const freshness = inspectDistFreshness(
SPEC_DIR,
'check',
'pnpm --filter @objectstack/spec check:skill-examples',
);
if (!freshness.fresh) {
console.error(freshness.message);
process.exit(1);
}

const { paths, missing } = specPaths();
if (missing.some((m) => m.endsWith('index.d.ts')) && !fs.existsSync(paths['@objectstack/spec']?.[0] ?? '')) {
fail(
Expand Down
Loading
Loading