@afokapu/atdd-bun 0.10.92 → 0.10.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -25,19 +25,10 @@
|
|
|
25
25
|
// suite. Both levels are parsed here.
|
|
26
26
|
import { excludedPath } from "../../lib/scan.mjs";
|
|
27
27
|
import { readFileSync, statSync, readdirSync, writeFileSync } from "node:fs";
|
|
28
|
-
import {
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { join } from "node:path";
|
|
30
29
|
|
|
31
30
|
export const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", ".next", ".git", "_generated"];
|
|
32
31
|
export const TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
33
|
-
|
|
34
|
-
// atdd-bun generates atdd-bun.integrity.test.ts and forbids editing it, so no tester rule judges it. It is recognised by
|
|
35
|
-
// its content, the package's template with the version filled in, never by name or header: a copy of either earns nothing.
|
|
36
|
-
const INTEGRITY_TEMPLATE = (() => { try { return readFileSync(join(dirname(fileURLToPath(import.meta.url)), "../../templates/agents/atdd-bun.integrity.test.ts"), "utf8"); } catch { return null; } })();
|
|
37
|
-
function isGeneratedIntegrityTest(path) {
|
|
38
|
-
if (!INTEGRITY_TEMPLATE || !path.endsWith("atdd-bun.integrity.test.ts")) return false;
|
|
39
|
-
try { return readFileSync(path, "utf8").replace(/^(\/\/ Generated by @afokapu\/atdd-bun )\S+?(\. Do not edit)/, "$1{{VERSION}}$2") === INTEGRITY_TEMPLATE; } catch { return false; }
|
|
40
|
-
}
|
|
41
32
|
export const PHASES = new Set(["RED", "UNIT", "SMOKE", "E2E"]);
|
|
42
33
|
export const LAYERS = new Set(["domain", "application", "integration", "presentation", "assembly"]);
|
|
43
34
|
|
|
@@ -59,7 +50,7 @@ function isExcluded(path, excludes) {
|
|
|
59
50
|
export function* walkTests(root, excludes) {
|
|
60
51
|
let st;
|
|
61
52
|
try { st = statSync(root); } catch { return; }
|
|
62
|
-
if (st.isFile()) { if (TEST_RE.test(root)
|
|
53
|
+
if (st.isFile()) { if (TEST_RE.test(root)) yield root; return; }
|
|
63
54
|
let names;
|
|
64
55
|
try { names = readdirSync(root); } catch { return; }
|
|
65
56
|
for (const name of names) {
|
|
@@ -68,7 +59,7 @@ export function* walkTests(root, excludes) {
|
|
|
68
59
|
let cst;
|
|
69
60
|
try { cst = statSync(full); } catch { continue; }
|
|
70
61
|
if (cst.isDirectory()) yield* walkTests(full, excludes);
|
|
71
|
-
else if (TEST_RE.test(full)
|
|
62
|
+
else if (TEST_RE.test(full)) yield full;
|
|
72
63
|
}
|
|
73
64
|
}
|
|
74
65
|
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.94",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -711,7 +711,7 @@
|
|
|
711
711
|
"detectors/bun_tester_discipline_detector/fixtures/dirty/unresolvable_binding.test.ts": "fd878356127cd66c05a1ab2c461982366f3a757895decf19cf24634f2c476d44",
|
|
712
712
|
"detectors/bun_tester_discipline_detector/fixtures/dirty/vacuous.red.test.ts": "deea6befc7f87aa54b52df40f1032e4b547cb1336da3aec985c76aeb5097364a",
|
|
713
713
|
"detectors/bun_tester_discipline_detector/fixtures/dirty/wrongruntime.test.ts": "041e421ef2b2810545020b5945733822dcae416d3360657217d3bab901cc2cdd",
|
|
714
|
-
"detectors/bun_tester_discipline_detector/test_header.mjs": "
|
|
714
|
+
"detectors/bun_tester_discipline_detector/test_header.mjs": "830f85456da04bfcb74332ecf7419373a9802ef42f04aa9ea06e2ba9206bfc03",
|
|
715
715
|
"detectors/bun_ts_metrics_detector/atdd.implementation.yaml": "6ced804a5fdc82b6cd52be21ebacefa5d3261fca03a391178b2bf21186776364",
|
|
716
716
|
"detectors/bun_ts_metrics_detector/checks/_map.json": "ed7c198564a80252f92e8ba2f50352daa1476ea88a23bfc0b3630e58c688f95d",
|
|
717
717
|
"detectors/bun_ts_metrics_detector/checks/ts_complexity_cyclomatic.mjs": "56640b3a0f565c45a73b7c174adb1d6b4b6ac24d286a907bc1426f013cd6039e",
|
|
@@ -1263,7 +1263,7 @@
|
|
|
1263
1263
|
"src/enforce.ts": "e0c10519aeecd7080f2900ea230fad2f09482af47c1cee885d5de71573e29732",
|
|
1264
1264
|
"src/hooks.ts": "b96b5fee3a03c2f4c317d6725d6d21ed3133db18d0cdcdda72183cf05c7b6639",
|
|
1265
1265
|
"src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
|
|
1266
|
-
"src/integrity.ts": "
|
|
1266
|
+
"src/integrity.ts": "f1ede7cb04924ae88e11917047277e264af6596b9759e381bd9c497cd34a797d",
|
|
1267
1267
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
1268
1268
|
"src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
|
|
1269
1269
|
"src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
|
|
@@ -1276,7 +1276,8 @@
|
|
|
1276
1276
|
"src/telemetry-plan.ts": "cdb1e8bfff414f3ea81977388973da2cf6d89567d9e060dc9d5989a33600f153",
|
|
1277
1277
|
"src/topology.ts": "9779ee48e1c453abb76363163d4ab363463fa14e359571139f6d1f1fcc51ae2c",
|
|
1278
1278
|
"templates/agents/AGENTS.block.md": "84d19dfabedb1e05ccfb7b19a38644e87f193ae2b74755897f75f13971bb687e",
|
|
1279
|
-
"templates/agents/atdd-bun.integrity.
|
|
1279
|
+
"templates/agents/atdd-bun.integrity.acceptance.yaml": "3fec7c6236c9bf7a5af2a9aeba2dc62e48319f38157a37a69b41cca675277d92",
|
|
1280
|
+
"templates/agents/atdd-bun.integrity.test.ts": "15ba901c49792427f790255f7b28b55a61ef955bd101209cbe087d8421ad666f",
|
|
1280
1281
|
"templates/docs/site.css": "482b3427e3a3cb02a85dc85cfbeac80592dfd53abf42fc92d6ea7071f22afc95",
|
|
1281
1282
|
"templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
|
|
1282
1283
|
"templates/github/atdd-bun.yml": "6b766ae25ac86f3c87c91b31726461d0ba13d3383e55bbc9c9d1fe79ec26fcba"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afokapu/atdd-bun",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.94",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/afokapu/atdd-bun.git"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"bun": ">=1.3.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@afokapu/atdd-flow": "^0.3.
|
|
38
|
+
"@afokapu/atdd-flow": "^0.3.49",
|
|
39
39
|
"ajv": "^8.17.1",
|
|
40
40
|
"ajv-formats": "^3.0.1",
|
|
41
41
|
"typescript": "6.0.3"
|
package/src/integrity.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { instructionPaths } from "./agent";
|
|
5
5
|
import { renderWorkflow } from "./ci";
|
|
6
6
|
import { defaultHookPolicy, enforcementIdentity, hookEvents, hooksStatus, isToolkitSourceCheckout, lockedToolkit, renderHook, type HookPolicy } from "./hooks";
|
|
7
|
+
import { topologyFor } from "./topology";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Integrity: the files an agent could change to weaken enforcement must stay canonical.
|
|
@@ -18,6 +19,10 @@ const PACKAGE = "@afokapu/atdd-bun";
|
|
|
18
19
|
const ownRoot = resolve(import.meta.dir, "..");
|
|
19
20
|
export const MANIFEST = "integrity.json";
|
|
20
21
|
export const TEST_FILE = "atdd-bun.integrity.test.ts";
|
|
22
|
+
export const CATALOG_FILE = "atdd-bun.integrity.acceptance.yaml";
|
|
23
|
+
const INTEGRITY_ACCEPTANCE = "acc:atdd-bun:E001-UNIT-001-enforcement-surfaces-canonical";
|
|
24
|
+
const CATALOG_MARKER = "# atdd-bun generated integrity catalog";
|
|
25
|
+
const CATALOG_POINTER = "// atdd-bun integrity catalog: ";
|
|
21
26
|
const WORKFLOW = ".github/workflows/atdd-bun.yml";
|
|
22
27
|
const BLOCK = /<!-- atdd-bun:start[\s\S]*?<!-- atdd-bun:end -->/;
|
|
23
28
|
// Agent blocks are guidance rather than enforcement surfaces, so their writer version remains upgrade-tolerant.
|
|
@@ -31,6 +36,10 @@ export async function testFilePath(repo: string) {
|
|
|
31
36
|
const root = text.split(/^\[/m).find(section => section.startsWith("test]"))?.match(/^\s*root\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
32
37
|
return join(repo, root ?? ".", TEST_FILE);
|
|
33
38
|
}
|
|
39
|
+
/** Where the generated integrity test's acceptance is declared: beneath the configured plan root. */
|
|
40
|
+
export async function catalogFilePath(repo: string) {
|
|
41
|
+
return join(repo, (await topologyFor(repo)).planRoot, CATALOG_FILE);
|
|
42
|
+
}
|
|
34
43
|
const git = async (root: string, args: string[]) => { const child = Bun.spawn({ cmd: ["git", ...args], cwd: root, stdout: "pipe", stderr: "pipe" }); const out = (await new Response(child.stdout).text()).trim(); return { code: await child.exited, out }; };
|
|
35
44
|
|
|
36
45
|
async function filesBelow(dir: string, base = dir): Promise<string[]> {
|
|
@@ -92,26 +101,43 @@ async function checkDependency(root: string): Promise<IntegrityFinding[]> {
|
|
|
92
101
|
return findings;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
async function
|
|
104
|
+
async function renderIntegrityPair(root: string, packageRoot: string) {
|
|
96
105
|
const version = (await Bun.file(join(packageRoot, "package.json")).json() as { version: string }).version;
|
|
97
|
-
const { digest } = await enforcementIdentity(root);
|
|
98
|
-
|
|
106
|
+
const { digest } = await enforcementIdentity(root), catalogPath = await catalogFilePath(root);
|
|
107
|
+
const catalog = relative(root, catalogPath).split(sep).join("/");
|
|
108
|
+
const render = async (file: string) => (await readFile(join(packageRoot, "templates/agents", file), "utf8"))
|
|
109
|
+
.replaceAll("{{VERSION}}", version).replaceAll("{{IDENTITY}}", digest).replaceAll("{{CATALOG}}", catalog);
|
|
110
|
+
return { test: await render(TEST_FILE), catalog: await render(CATALOG_FILE), catalogPath };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Surface = { file: string; path: string; expected: (() => Promise<string>) | null };
|
|
114
|
+
const RESTORE_GENERATED = "bun run atdd-bun init --replace";
|
|
115
|
+
|
|
116
|
+
/** A generated surface is missing, or, when an expected rendering is given, stale against it. */
|
|
117
|
+
async function surfaceFinding({ file, path, expected }: Surface): Promise<IntegrityFinding | null> {
|
|
118
|
+
if (!existsSync(path)) return { file, detail: "is missing", restore: RESTORE_GENERATED };
|
|
119
|
+
if (expected && canonical(await readFile(path, "utf8")) !== canonical(await expected())) return { file, detail: "is stale; it must match the exact lock/config/profile identity", restore: RESTORE_GENERATED };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The catalog path comes from the configured plan root, or, uncompared, only from the generated test's exact pointer. */
|
|
124
|
+
async function integrityCatalogPath(root: string, testPath: string, pair: { catalogPath: string } | null) {
|
|
125
|
+
if (pair) return pair.catalogPath;
|
|
126
|
+
return existsSync(testPath) ? priorCatalogPath(root, await readFile(testPath, "utf8")) : null;
|
|
99
127
|
}
|
|
100
128
|
|
|
101
129
|
/** The generated enforcement surfaces are byte-identical to this exact lock/config/profile identity. */
|
|
102
130
|
async function checkEnforcementGenerated(root: string, packageRoot: string, compare = true): Promise<IntegrityFinding[]> {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
if (!existsSync(testPath)) findings.push({ file: test, detail: "is missing", restore });
|
|
114
|
-
else if (compare && canonical(await readFile(testPath, "utf8")) !== canonical(await renderIntegrityTest(root, packageRoot))) findings.push({ file: test, detail: "is stale; it must match the exact lock/config/profile identity", restore });
|
|
131
|
+
// Without comparison the config may be unreadable: render nothing that depends on it, only check presence.
|
|
132
|
+
const pair = compare ? await renderIntegrityPair(root, packageRoot) : null;
|
|
133
|
+
const surfaces: Surface[] = hookEvents.map(event => ({ file: `.githooks/${event}`, path: join(root, ".githooks", event), expected: compare ? () => renderHook(event, root) : null }));
|
|
134
|
+
surfaces.push({ file: WORKFLOW, path: join(root, WORKFLOW), expected: compare ? () => renderWorkflow(root) : null });
|
|
135
|
+
const testPath = await testFilePath(root);
|
|
136
|
+
surfaces.push({ file: relative(root, testPath), path: testPath, expected: pair ? async () => pair.test : null });
|
|
137
|
+
const catalogPath = await integrityCatalogPath(root, testPath, pair);
|
|
138
|
+
if (catalogPath) surfaces.push({ file: relative(root, catalogPath).split(sep).join("/"), path: catalogPath, expected: pair ? async () => pair.catalog : null });
|
|
139
|
+
const findings: IntegrityFinding[] = [];
|
|
140
|
+
for (const surface of surfaces) { const finding = await surfaceFinding(surface); if (finding) findings.push(finding); }
|
|
115
141
|
return findings;
|
|
116
142
|
}
|
|
117
143
|
|
|
@@ -278,12 +304,42 @@ export function formatIntegrity(findings: IntegrityFinding[]): string {
|
|
|
278
304
|
].join("\n");
|
|
279
305
|
}
|
|
280
306
|
|
|
281
|
-
|
|
307
|
+
function priorCatalogPath(root: string, generatedTest: string): string | null {
|
|
308
|
+
const pointer = generatedTest.split(/\r?\n/).find(line => line.startsWith(CATALOG_POINTER))?.slice(CATALOG_POINTER.length);
|
|
309
|
+
if (!pointer || pointer.startsWith("/") || pointer.replaceAll("\\", "/").split("/").includes("..") || basename(pointer) !== CATALOG_FILE) return null;
|
|
310
|
+
const path = resolve(root, pointer);
|
|
311
|
+
return path.startsWith(`${resolve(root)}${sep}`) ? path : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isOwnedCatalog(text: string): boolean {
|
|
315
|
+
return text.includes(CATALOG_MARKER) && text.includes(`urn: ${INTEGRITY_ACCEPTANCE}`) && text.startsWith("# Generated by @afokapu/atdd-bun ");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Write the generated integrity test and its plan binding into a repository. */
|
|
282
319
|
export async function integrityInit(repo = process.cwd(), replace = false) {
|
|
283
|
-
const output = await testFilePath(
|
|
320
|
+
const root = resolve(repo), output = await testFilePath(root);
|
|
284
321
|
if (existsSync(output) && !replace) return { ok: false, message: `${output} exists; use --replace` };
|
|
322
|
+
const previous = existsSync(output) ? priorCatalogPath(root, await readFile(output, "utf8")) : null;
|
|
323
|
+
const pair = await renderIntegrityPair(root, ownRoot);
|
|
324
|
+
if (previous && previous !== pair.catalogPath && existsSync(previous)) {
|
|
325
|
+
const text = await readFile(previous, "utf8").catch(() => "");
|
|
326
|
+
if (isOwnedCatalog(text)) await rm(previous);
|
|
327
|
+
}
|
|
285
328
|
await mkdir(dirname(output), { recursive: true });
|
|
286
|
-
await
|
|
329
|
+
await mkdir(dirname(pair.catalogPath), { recursive: true });
|
|
330
|
+
await writeFile(pair.catalogPath, pair.catalog);
|
|
331
|
+
await writeFile(output, pair.test);
|
|
287
332
|
return { ok: true, message: output };
|
|
288
333
|
}
|
|
289
|
-
export async function integrityStatus(repo = process.cwd()) {
|
|
334
|
+
export async function integrityStatus(repo = process.cwd()) {
|
|
335
|
+
const root = resolve(repo), output = await testFilePath(root);
|
|
336
|
+
if (!existsSync(output)) return { ok: false, message: `${output} is missing; run: bun run atdd-bun init --replace` };
|
|
337
|
+
try {
|
|
338
|
+
const pair = await renderIntegrityPair(root, ownRoot);
|
|
339
|
+
if (canonical(await readFile(output, "utf8")) !== canonical(pair.test)) return { ok: false, message: `${output} is stale; run: bun run atdd-bun init --replace` };
|
|
340
|
+
if (!existsSync(pair.catalogPath)) return { ok: false, message: `${pair.catalogPath} is missing; run: bun run atdd-bun init --replace` };
|
|
341
|
+
return canonical(await readFile(pair.catalogPath, "utf8")) === canonical(pair.catalog)
|
|
342
|
+
? { ok: true, message: output }
|
|
343
|
+
: { ok: false, message: `${pair.catalogPath} is stale; run: bun run atdd-bun init --replace` };
|
|
344
|
+
} catch (error) { return { ok: false, message: `${output} cannot be verified: ${String(error)}; run: bun run atdd-bun init --replace` }; }
|
|
345
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Generated by @afokapu/atdd-bun {{VERSION}}. Do not edit; run `atdd-bun init --replace`.
|
|
2
|
+
# atdd-bun enforcement identity: {{IDENTITY}}
|
|
3
|
+
# atdd-bun generated integrity catalog
|
|
4
|
+
acceptances:
|
|
5
|
+
- identity:
|
|
6
|
+
urn: acc:atdd-bun:E001-UNIT-001-enforcement-surfaces-canonical
|
|
7
|
+
id: E001-UNIT-001-enforcement-surfaces-canonical
|
|
8
|
+
purpose: Verify the installed toolkit, policy, and generated enforcement surfaces remain canonical.
|
|
9
|
+
phase: GREEN
|
|
10
|
+
harness:
|
|
11
|
+
type: unit
|
|
12
|
+
category: backend
|
|
13
|
+
language_hint: typescript
|
|
14
|
+
given:
|
|
15
|
+
abstract:
|
|
16
|
+
- The repository has installed ATDD Bun and generated its enforcement surfaces.
|
|
17
|
+
when:
|
|
18
|
+
abstract: The generated integrity test inspects the installed toolkit, policy, and generated files.
|
|
19
|
+
action: invoke
|
|
20
|
+
target: registerIntegrityTest
|
|
21
|
+
then:
|
|
22
|
+
abstract:
|
|
23
|
+
- Every protected enforcement surface matches the exact installed package and policy identity.
|
|
24
|
+
assertions:
|
|
25
|
+
- type: equals
|
|
26
|
+
target: integrity finding count
|
|
27
|
+
value: 0
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// Generated by @afokapu/atdd-bun {{VERSION}}. Do not edit; run `atdd-bun integrity init --replace`.
|
|
2
2
|
// atdd-bun enforcement identity: {{IDENTITY}}
|
|
3
|
+
// atdd-bun integrity catalog: {{CATALOG}}
|
|
4
|
+
// URN: test:atdd-bun:integrity:E001-UNIT-001-enforcement-surfaces-canonical
|
|
5
|
+
// Acceptance: acc:atdd-bun:E001-UNIT-001-enforcement-surfaces-canonical
|
|
6
|
+
// Phase: UNIT
|
|
7
|
+
// Layer: integration
|
|
3
8
|
// Fails loudly when the installed package, its generated files, or atdd-bun.yaml were changed.
|
|
4
9
|
import { registerIntegrityTest } from "@afokapu/atdd-bun/register";
|
|
5
10
|
|