Skip to content

Commit 5ba5a47

Browse files
SisyphusZhengDevBot
andauthored
refactor(governance): close M15 decision-test residual + L12 least-privilege docs checker (#1230 B2.8) (#1296)
* refactor(governance): close M15 decision-test residual and L12 least-privilege docs checker (#1230 B2.8) * feat(governance): gate release-line truth in ci/release tiers (#1230 B2.8 thinker follow-up) --------- Co-authored-by: DevBot <devbot@openelement.dev>
1 parent 18221ee commit 5ba5a47

11 files changed

Lines changed: 520 additions & 185 deletions

deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@
5555
"docs:check-claims": "deno run --allow-read tools/check-docs-truth.ts --check=claims",
5656
"docs:check-recipe-parity": "deno run --allow-read tools/check-supabase-recipe-parity.ts",
5757
"release:evidence:check": "deno run --allow-read --allow-run=git tools/check-docs-truth.ts --check=evidence",
58+
"release:truth:check": "deno run --allow-read tools/check-release-truth.ts",
5859
"release:state-machine:check": "deno run --allow-read --allow-run=git tools/check-release-state-machine.ts",
5960
"docs:check-version-anchors": "deno run --allow-read tools/check-version-anchors.ts",
60-
"docs:truth": "deno run --allow-read --allow-run=git tools/check-docs-truth.ts && deno run --allow-read tools/check-release-truth.ts && deno task docs:check-version-anchors && deno task docs:check-recipe-parity",
61+
"docs:truth": "deno run --allow-read --allow-run=git tools/check-docs-truth.ts && deno task release:truth:check && deno task docs:check-version-anchors && deno task docs:check-recipe-parity",
6162
"www:check-current-truth": "deno run --allow-read tools/check-docs-truth.ts --check=www",
6263
"www:check-theme-tokens": "deno run --allow-read tools/check-www-theme-tokens.ts",
6364
"www:check-artifact-truth": "deno run --allow-read tools/check-docs-truth.ts --check=www --artifacts",
@@ -76,7 +77,7 @@
7677
"lint:markdown": "deno run -A npm:markdownlint-cli2@0.23.2 \"**/*.md\"",
7778
"deno-api:check": "deno run --allow-read --allow-env tools/check-deno-api-free.ts",
7879
"text-integrity:check": "deno run --allow-read --allow-run=git tools/check-docs-truth.ts --check=text",
79-
"audit:citations:check": "deno run -A tools/check-audit-citations.ts",
80+
"audit:citations:check": "deno run --allow-read --allow-run=git tools/check-audit-citations.ts",
8081
"graph:check": "deno run --allow-read --allow-env tools/check-package-graph.ts",
8182
"consumer:local": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-local.ts",
8283
"consumer:packaged": "deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-packaged-starter.ts && deno run --allow-read --allow-write --allow-run --allow-env --allow-net tools/consumer-local.ts --packaged-import-map-check",

docs/governance/PROJECT_WORKFLOW.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,13 @@ npm's default `latest` tag. `tools/verify-npm-release.ts` asserts
156156
`deno task workflow:check` verifies that the workflow itself remains visible and
157157
that the active version plan has the required shape. AutoFlow3 is the single
158158
gate and evidence control plane for hooks and CI.
159+
160+
Gate ownership (#1230): `tools/autoflow/policy.ts` is the machine-readable gate
161+
registry — each gate names exactly one deno task, and each task names its owning
162+
script. Generic toolchain concerns (format, lint, type graph, Markdown
163+
structure, secret content, workflow lint/security) are owned by the pinned OSS
164+
tools themselves and wired as plain CI steps and git-hook calls (ADR-0144), not
165+
as AutoFlow gates. Registry integrity — every gate resolving to an existing
166+
task, and no two gates sharing one command — is asserted in
167+
`tools/autoflow/__tests__/policy.test.ts`, so this document deliberately does
168+
not duplicate the gate list.

tools/autoflow/__tests__/policy.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ Deno.test('policy: release tier includes publish dry-run and nitro proofs', () =
9797
assert(gates.includes('third-party-wc:smoke'));
9898
});
9999

100+
Deno.test('policy: release-line truth is gated in ci and release tiers (#1230)', () => {
101+
// check-release-truth.ts (release-state.json consistency + README/STATUS/
102+
// ROADMAP registry anchors) must not depend on the local docs:truth
103+
// composition alone — the CI-GATING rule requires a policy gate.
104+
for (const tier of ['ci', 'release'] as const) {
105+
const gates = selectGates(tier, ['docs/release/release-state.json']).map((gate) => gate.name);
106+
assert(gates.includes('release:truth:check'), `release:truth:check missing from ${tier} tier`);
107+
}
108+
const gate = allRegisteredGates().find((candidate) => candidate.name === 'release:truth:check');
109+
assert(gate, 'release:truth:check must be registered');
110+
assertEquals(gate.command, ['deno', 'task', 'release:truth:check']);
111+
});
112+
100113
Deno.test('policy: package artifacts gate packs before the packaged consumer runs', () => {
101114
const gates = selectGates('ci', ['packages/element/src/index.ts']).map((gate) => gate.name);
102115
assert(gates.indexOf('package-artifacts:check') < gates.indexOf('consumer:packaged'));
@@ -388,3 +401,61 @@ Deno.test('release: patch release plan omits publish and GitHub release outside
388401
else Deno.env.set('CI', originalCi);
389402
}
390403
});
404+
405+
Deno.test('policy: every gate command resolves to an existing deno task (#1230)', async () => {
406+
// policy.ts is the machine-readable gate registry; this assertion is the
407+
// drift guard that keeps gate -> task -> owning script referentially intact.
408+
const denoJson = JSON.parse(await Deno.readTextFile('deno.json')) as {
409+
tasks: Record<string, string>;
410+
};
411+
const gates = allRegisteredGates();
412+
assert(gates.length > 0);
413+
for (const gate of gates) {
414+
assertEquals(
415+
gate.command.slice(0, 2),
416+
['deno', 'task'],
417+
`${gate.name} must invoke a deno task`,
418+
);
419+
const task = gate.command[2];
420+
assert(task in denoJson.tasks, `${gate.name} references missing deno task "${task}"`);
421+
}
422+
});
423+
424+
Deno.test('policy: no two gates share the same command (#1230)', () => {
425+
// One concern, one owner: two gates on the same command would double-run
426+
// the same check and fork its ownership. Parameterized gates (same task,
427+
// different args — e.g. per-browser smoke) are distinct concerns.
428+
const seen = new Map<string, string>();
429+
for (const gate of allRegisteredGates()) {
430+
const command = gate.command.join(' ');
431+
assertEquals(
432+
seen.get(command),
433+
undefined,
434+
`gates "${seen.get(command)}" and "${gate.name}" both own command "${command}"`,
435+
);
436+
seen.set(command, gate.name);
437+
}
438+
});
439+
440+
function allRegisteredGates() {
441+
const byName = new Map<string, ReturnType<typeof selectGates>[number]>();
442+
for (const tier of ['dev', 'push', 'ci', 'release'] as const) {
443+
// ci/release selection ignores triggers; a maximally-broad changed-path
444+
// set additionally captures dev/push-only triggered gates.
445+
const changedPaths = [
446+
'packages/element/src/index.ts',
447+
'docs/current/VERSION_PLAN.md',
448+
'www/app/main.tsx',
449+
'tools/autoflow/policy.ts',
450+
'.github/workflows/autoflow-ci.yml',
451+
'examples/supabase-cloudflare-starter/deno.json',
452+
'deno.json',
453+
'README.md',
454+
'e2e/starter-smoke/setup.ts',
455+
];
456+
for (const gate of selectGates(tier, changedPaths)) {
457+
byName.set(gate.name, gate);
458+
}
459+
}
460+
return [...byName.values()];
461+
}

tools/autoflow/policy.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,26 @@ const GATES: readonly GateDefinition[] = [
239239
/^deno\.json$/,
240240
],
241241
},
242+
{
243+
// #1230 (B2.8): check-release-truth.ts (release-state.json consistency +
244+
// README/STATUS/ROADMAP registry anchors) previously ran only inside the
245+
// local docs:truth composition — a release-truth check with no CI wiring
246+
// violates the CI-GATING rule. Same command as the composition, matching
247+
// the sibling release:evidence:check tier declaration.
248+
name: 'release:truth:check',
249+
command: ['deno', 'task', 'release:truth:check'],
250+
tiers: ['ci', 'release'],
251+
triggers: [
252+
/^docs\/release\/release-state\.json$/,
253+
/^docs\/(status|roadmap|current)\//,
254+
/^README/,
255+
/^examples\/supabase-cloudflare-starter\/deno\.json$/,
256+
/^tools\/check-release-truth(?:\.test)?\.ts$/,
257+
/^tools\/project-constants\.ts$/,
258+
/^tools\/lib\/version\.ts$/,
259+
/^deno\.json$/,
260+
],
261+
},
242262
{
243263
// Replays the durable autoflow3 release state machine recorded under
244264
// docs/release/autoflow3/<tag>.json from git history and fails unless the
@@ -466,7 +486,7 @@ const GATES: readonly GateDefinition[] = [
466486
triggers: [
467487
/^www\/e2e\/visual-baselines\.spec\.ts$/,
468488
/^www\/e2e\/visual-baselines\.spec\.ts-snapshots\//,
469-
/^tools\/check-visual-baseline-duplicates\.ts$/,
489+
/^tools\/check-visual-baseline-duplicates(?:\.test)?\.ts$/,
470490
/^deno\.json$/,
471491
],
472492
},

tools/check-audit-citations.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
* `packages/adapter-vite/src/internal/ssg/ssg-render.ts`). Ambiguous
1313
* basenames (e.g. `index.ts`) are flagged rather than guessed.
1414
*
15-
* Usage:
16-
* deno run -A tools/check-audit-citations.ts [files...] [--sha=<commit>]
17-
* deno run -A tools/check-audit-citations.ts --write # append a verification appendix
15+
* Usage (least privilege — L12/#1230; the `audit:citations:check` task runs
16+
* without --write, so it needs no write permission):
17+
* deno run --allow-read --allow-run=git tools/check-audit-citations.ts [files...] [--sha=<commit>]
18+
* deno run --allow-read --allow-write --allow-run=git tools/check-audit-citations.ts --write
1819
*
1920
* With no file arguments the tool scans docs/audit/ for reports archived under
2021
* the YYYY-MM-DD-* naming convention. Archived reports are verified against

tools/check-repo-hygiene.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { assertEquals } from '@std/assert';
2+
import {
3+
classifyTrackedBinary,
4+
credentialFileFailure,
5+
isActiveScanFile,
6+
isAllowedRemovedPackageMention,
7+
isAllowedTrackedIgnored,
8+
isForbiddenRootTracked,
9+
isForbiddenUntrackedResidue,
10+
LARGE_BINARY_LIMIT_BYTES,
11+
} from './check-repo-hygiene.ts';
12+
13+
// M15 (#1230): the hygiene gate's allow/deny classification is decision logic
14+
// that can turn a failure into a success (a too-broad template carve-out or
15+
// allowlist silently greens a tracked credential or binary). Pin it.
16+
17+
Deno.test('hygiene: tracked credential files fail, placeholder templates pass', () => {
18+
// Real credentials are always failures.
19+
assertEquals(typeof credentialFileFailure('.env'), 'string');
20+
assertEquals(typeof credentialFileFailure('packages/app/.env'), 'string');
21+
assertEquals(typeof credentialFileFailure('.env.production'), 'string');
22+
assertEquals(typeof credentialFileFailure('certs/server.pem'), 'string');
23+
assertEquals(typeof credentialFileFailure('.ssh/id_rsa'), 'string');
24+
assertEquals(typeof credentialFileFailure('id_rsa.pub'), 'string');
25+
// The template carve-out is exact: only .env.example/.sample/.template.
26+
assertEquals(credentialFileFailure('.env.example'), undefined);
27+
assertEquals(credentialFileFailure('examples/x/.env.sample'), undefined);
28+
assertEquals(credentialFileFailure('.env.template'), undefined);
29+
// Non-credentials pass.
30+
assertEquals(credentialFileFailure('README.md'), undefined);
31+
assertEquals(credentialFileFailure('tools/environment.ts'), undefined);
32+
});
33+
34+
Deno.test('hygiene: large tracked binaries fail outside the allowed asset dirs', () => {
35+
const over = LARGE_BINARY_LIMIT_BYTES + 1;
36+
// Over-limit binaries are failures outside the allowlist...
37+
assertEquals(typeof classifyTrackedBinary('packages/element/logo.png', over), 'string');
38+
// ...and allowed in the intentional asset directories.
39+
assertEquals(classifyTrackedBinary('www/design/mockups/home.png', over), undefined);
40+
assertEquals(
41+
classifyTrackedBinary('www/e2e/visual-baselines.spec.ts-snapshots/home.png', over),
42+
undefined,
43+
);
44+
assertEquals(classifyTrackedBinary('examples/x/fixtures/banner.mp4', over), undefined);
45+
assertEquals(classifyTrackedBinary('www/public/assets/dragon-hero.mp4', over), undefined);
46+
// Under the limit or non-binary extensions are not this check's concern.
47+
assertEquals(classifyTrackedBinary('packages/element/logo.png', 1024), undefined);
48+
assertEquals(classifyTrackedBinary('packages/element/big.ts', over), undefined);
49+
});
50+
51+
Deno.test('hygiene: root generated artifacts are tracked-file failures, nested ones are not', () => {
52+
assertEquals(isForbiddenRootTracked('dist/server/index.js'), true);
53+
assertEquals(isForbiddenRootTracked('playwright-report/index.html'), true);
54+
assertEquals(isForbiddenRootTracked('debug.log'), true);
55+
// Anchored at the repo root: package-level build output is gitignored, not
56+
// this tripwire's concern.
57+
assertEquals(isForbiddenRootTracked('packages/element/dist/mod.js'), false);
58+
assertEquals(isForbiddenRootTracked('packages/element/src/mod.ts'), false);
59+
});
60+
61+
Deno.test('hygiene: untracked workflow residue fails, other untracked files pass', () => {
62+
assertEquals(isForbiddenUntrackedResidue('.github/workflows/debug.yml'), true);
63+
assertEquals(isForbiddenUntrackedResidue('hub-submission.json'), true);
64+
assertEquals(isForbiddenUntrackedResidue('notes.md'), false);
65+
});
66+
67+
Deno.test('hygiene: only vendored license attributions may be tracked-and-ignored', () => {
68+
assertEquals(isAllowedTrackedIgnored('vendor/jsr.io/@std/fs/LICENSE'), true);
69+
assertEquals(isAllowedTrackedIgnored('vendor/jsr.io/std/LICENSE'), true);
70+
assertEquals(isAllowedTrackedIgnored('vendor/jsr.io/@std/fs/mod.ts'), false);
71+
});
72+
73+
Deno.test('hygiene: removed-package mention scan covers active roots only', () => {
74+
assertEquals(isActiveScanFile('deno.json'), true);
75+
assertEquals(isActiveScanFile('packages/element/src/mod.ts'), true);
76+
assertEquals(isActiveScanFile('tools/check-repo-hygiene.ts'), true);
77+
// docs/audit, docs/release and other historical trees are not scanned.
78+
assertEquals(isActiveScanFile('docs/audit/2026-01-01-x.md'), false);
79+
assertEquals(isActiveScanFile('packages/element/README.png'), false);
80+
// The allowlist is exact-path, not substring.
81+
assertEquals(isAllowedRemovedPackageMention('tools/check-repo-hygiene.ts'), true);
82+
assertEquals(isAllowedRemovedPackageMention('tools/check-repo-hygiene-extra.ts'), false);
83+
});

0 commit comments

Comments
 (0)