@afokapu/atdd-bun 0.10.6 → 0.10.8
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.
- package/README.md +4 -0
- package/conventions/delivery/delivery.approved-sha-resolves.convention.yaml +1 -1
- package/conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml +1 -1
- package/detectors/htmx_e2e_detector/checks/_e2e.mjs +4 -1
- package/integrity.json +7 -7
- package/package.json +1 -1
- package/src/delivery.ts +9 -5
- package/src/hooks.ts +5 -1
package/README.md
CHANGED
|
@@ -171,6 +171,10 @@ From 0.10.5 only coordinators and drivers use the board: a writer or reviewer ta
|
|
|
171
171
|
launch prompt and returns its answer as output, which the driver posts. `atdd-bun chat wait` takes several
|
|
172
172
|
topics and `--skip heartbeat`; reviewers answer in JSON.
|
|
173
173
|
|
|
174
|
+
From 0.10.7 only a browser spec (a `*.e2e.*` file, or a Playwright file bound by `// Train:`/`// Journey:` or a
|
|
175
|
+
`test:train:`/`test:journey:` URN) is a journey spec. A `bun:test` file may carry the `Acceptance:` line
|
|
176
|
+
traceability needs, including a test proving `acc:train:…` and the journey test `atdd-bun.topology.e2e-location` requires.
|
|
177
|
+
|
|
174
178
|
Every key is optional; these are the defaults:
|
|
175
179
|
|
|
176
180
|
```yaml
|
|
@@ -18,7 +18,7 @@ content:
|
|
|
18
18
|
Record the full or abbreviated SHA the final review named. After a rebase, run the final review again on the new head.
|
|
19
19
|
exceptions:
|
|
20
20
|
- >-
|
|
21
|
-
At the gate, a record the change does not touch is not judged again: it was judged when it merged, and its tranche branch may be gone.
|
|
21
|
+
At the gate, a record the change does not touch is not judged again: it was judged when it merged, and its tranche branch may be gone. The pre-push hook scopes the same way, from where the branch left its base branch; with no base found it judges every record. Everywhere else every ready record is judged.
|
|
22
22
|
- >-
|
|
23
23
|
A shallow clone may lack the commit: CI must fetch full history (the generated workflow uses fetch-depth 0).
|
|
24
24
|
metadata:
|
|
@@ -3,7 +3,7 @@ rule_id: tester.htmx.journey-no-acceptance-marker
|
|
|
3
3
|
kind: rule
|
|
4
4
|
status: active
|
|
5
5
|
name: A journey spec carries no Acceptance or WMBT header
|
|
6
|
-
statement: A journey spec MUST NOT carry `// Acceptance:` or `// WMBT:` header lines. Bun/htmx realization of `tester.vite.journey-no-acceptance-marker`,
|
|
6
|
+
statement: A journey spec, meaning a browser spec (a `*.e2e.*` file, or a Playwright file bound to a train or journey), MUST NOT carry `// Acceptance:` or `// WMBT:` header lines; a bun test file is never a journey spec. Bun/htmx realization of `tester.vite.journey-no-acceptance-marker`,
|
|
7
7
|
strict.
|
|
8
8
|
terms:
|
|
9
9
|
- term_id: browser_spec
|
|
@@ -65,7 +65,10 @@ export function collectSpecs(roots, excludes) {
|
|
|
65
65
|
return [...out.values()];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
// A journey spec is a browser spec: a *.e2e.* file, or a Playwright file whose header binds a train or journey. A bun:test
|
|
69
|
+
// file is never one, so a Bun test proving acc:train:… (or the journey test atdd-bun.topology.e2e-location requires) may
|
|
70
|
+
// carry the Acceptance: line traceability needs.
|
|
71
|
+
export const isJourneySpec = (spec) => spec.e2eNamed || (spec.playwright && spec.journeyMarked);
|
|
69
72
|
|
|
70
73
|
/** The plan subjects a spec can bind to, read with the package's own loader. */
|
|
71
74
|
export async function planOf(root) {
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.8",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "54f6959a5fe0a83ff7ba07856f5385eff0bff0607bfe7b2287897b2d8378825e",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml": "81c5c773d5ee15e8d99a2c237aa9846b110b2533df45ebf407d0e3ec6ed3dd03",
|
|
9
9
|
"conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml": "e96d7c1455d0c221072d82e7f2b55da1c4f9ceb17718eb6ed96affdeef37675b",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"conventions/coder.htmx/coder.htmx.verb-endpoint-is-routed.convention.yaml": "d6aa58d35df4b7ebbcf675ca79f0177d64e56f095802b9a3eaae3e4befb5c270",
|
|
92
92
|
"conventions/coder.htmx/coder.htmx.verb-endpoint-same-origin.convention.yaml": "2128d26153e2d1f19c65a1e1665d4f82c78395daedb336d6ef1256e82b74aedb",
|
|
93
93
|
"conventions/coder.htmx/coder.htmx.verb-mutation-signals-progress.convention.yaml": "ae85c5ea01a09a3c41b939988088a8e5da91848d2710a1f0a1e0c86308638ac9",
|
|
94
|
-
"conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "
|
|
94
|
+
"conventions/delivery/delivery.approved-sha-resolves.convention.yaml": "68f2ecc10870e780abb05bb31d211b1923d47b56672df4b3950f437aa9cf8ca8",
|
|
95
95
|
"conventions/delivery/delivery.board.convention.yaml": "51ff4c16dddf20504f34accc5ddcd150108fab70e4e6f5bdb36cec71d0c905a8",
|
|
96
96
|
"conventions/delivery/delivery.config-schema.convention.yaml": "4c7b449074a8bd9f89b70c702ab3e0df229105ddb4458772f894f0bf37813e9f",
|
|
97
97
|
"conventions/delivery/delivery.evidence-schema.convention.yaml": "9c6a22495dfb3e2fcbca700392866c3b5660084b1f5594230eb320b499d5e5cb",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"conventions/tester.htmx/tester.htmx.fragment-asserts-returned-markup.convention.yaml": "b8de8e893b582d8ec4b7c24acfa16da0eccd14e6e58e92baf9e9c10c9749ea0c",
|
|
151
151
|
"conventions/tester.htmx/tester.htmx.journey-binding-header.convention.yaml": "7a9f8655cccdd12ab7e2d49af128c9f9572ad441823ca6e32cbae26b26b2d714",
|
|
152
152
|
"conventions/tester.htmx/tester.htmx.journey-layer-assembly.convention.yaml": "86948c4b1e928eef07329afeec73660ea51878e3baa58350f61a8f81c20e9fc3",
|
|
153
|
-
"conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml": "
|
|
153
|
+
"conventions/tester.htmx/tester.htmx.journey-no-acceptance-marker.convention.yaml": "f0d96af21e057e8ae02bb0d3c3f6e10c136e5b3eb19d94a7d0f034bc0b0a87e0",
|
|
154
154
|
"conventions/tester.htmx/tester.htmx.journey-urn-format.convention.yaml": "6771d984ddb6fe564a6803e971e934242d0a53bd8711826e66d4d0c3283f8aed",
|
|
155
155
|
"conventions/tester.htmx/tester.htmx.presentation-smoke-coverage.convention.yaml": "b18565f55f9f92f7b292bae8231c2343e72f910dc9e228b307db318eb62488cc",
|
|
156
156
|
"conventions/tester.htmx/tester.htmx.responsive-harness.convention.yaml": "cec312935ab592c2c7a5c6a94623a02df771a9e2ca2035b721ef1006c4800fdf",
|
|
@@ -675,7 +675,7 @@
|
|
|
675
675
|
"detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/orphan/README.md": "7d480b1fff6546615e55d866784bc2267d06d0221dfde8d0ba0353fa1ea0db9a",
|
|
676
676
|
"detectors/delivery_evidence/fixtures/dirty/docs/delivery/tranches/ui/evidence.yaml": "777b4f7921e302dfce79cd764bba8b44091ac9c7ee04a00f5dbb663342ec68a0",
|
|
677
677
|
"detectors/htmx_e2e_detector/atdd.implementation.yaml": "454bb123039d57c992d27d9274e20fa3452ac41a237eb55471be62a4bcfebf06",
|
|
678
|
-
"detectors/htmx_e2e_detector/checks/_e2e.mjs": "
|
|
678
|
+
"detectors/htmx_e2e_detector/checks/_e2e.mjs": "efec6c8e3dc8b95bf3acd826397ecdedac64c06d367791a4017a272bc0660896",
|
|
679
679
|
"detectors/htmx_e2e_detector/checks/_map.json": "94dec08ff4764cd6a3dba139f12a7c984292e95a9354b0cdaa0ce9d4180e2c89",
|
|
680
680
|
"detectors/htmx_e2e_detector/checks/a11y_harness.mjs": "d66e2a6da3c3820db72fdaddcd53efb9c0e39d03bc559a59b800923d5c944700",
|
|
681
681
|
"detectors/htmx_e2e_detector/checks/covered_train_is_routed.mjs": "c7a8db0e932ba96489477fb5a8589cd9eba868db6fbaaa8012802a4da78fa0f8",
|
|
@@ -1195,10 +1195,10 @@
|
|
|
1195
1195
|
"src/board.ts": "115c6cff12fb2b20e6471013161aef020a65331ecb97fa323e9b08f55053149f",
|
|
1196
1196
|
"src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
|
|
1197
1197
|
"src/cli.ts": "e45f2666334503a91eb65b1b5ee4dd8acab6b99f62d333e2ee2b65963db6358b",
|
|
1198
|
-
"src/delivery.ts": "
|
|
1198
|
+
"src/delivery.ts": "5688356e26bf5aa49ad940d1bcd42e41b9f3dc1c21dc833b328fcb38a3f04fb8",
|
|
1199
1199
|
"src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
|
|
1200
1200
|
"src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
|
|
1201
|
-
"src/hooks.ts": "
|
|
1201
|
+
"src/hooks.ts": "2feeec2674c1462c94256de02ce59ccc6e3ff671ec4dca1bf9903c32888b600b",
|
|
1202
1202
|
"src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
|
|
1203
1203
|
"src/integrity.ts": "ba3de29e12e62a1b3e0149e243183fdfbce33d88efc9d39605b54996c01426cd",
|
|
1204
1204
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
package/package.json
CHANGED
package/src/delivery.ts
CHANGED
|
@@ -394,8 +394,10 @@ async function againstRef(root: string, ref: string): Promise<{ base: string; he
|
|
|
394
394
|
return since && head ? { base: since, head } : null;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
/** `gate: true` is the pre-merge gate; `false` disables it; unset reads ATDD_DELIVERY_GATE.
|
|
398
|
-
|
|
397
|
+
/** `gate: true` is the pre-merge gate; `false` disables it; unset reads ATDD_DELIVERY_GATE. `since` (unset: reads
|
|
398
|
+
* ATDD_DELIVERY_SINCE, which only the pre-push hook sets) scopes a run outside the gate as the gate is scoped: a record
|
|
399
|
+
* unchanged since that commit is not judged again. An unresolvable commit scopes nothing, so every record is judged. */
|
|
400
|
+
export type DeliveryOptions = { gate?: boolean | GateMode; base?: string; since?: string };
|
|
399
401
|
|
|
400
402
|
export async function validateDelivery(root = process.cwd(), options: DeliveryOptions = {}): Promise<PlanFinding[]> {
|
|
401
403
|
const absolute = resolve(root), config = await readConfig(absolute);
|
|
@@ -434,9 +436,11 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
|
|
|
434
436
|
const mode = options.gate === undefined ? gateMode() : options.gate === true ? "merge" : options.gate || null;
|
|
435
437
|
// At the gate, a record the change does not touch was judged when it merged. It is not judged again, against a
|
|
436
438
|
// later policy, schema or history: records are append-only and could never be repaired, so one tightening would
|
|
437
|
-
// fail every later change. Outside the gate every record is judged
|
|
438
|
-
// an untouched record's folder (mergeGate).
|
|
439
|
-
const
|
|
439
|
+
// fail every later change. Outside the gate every record is judged, unless a `since` commit scopes the run the same
|
|
440
|
+
// way (the pre-push hook). The gate itself still rejects any change to an untouched record's folder (mergeGate).
|
|
441
|
+
const since = options.since ?? process.env.ATDD_DELIVERY_SINCE;
|
|
442
|
+
const onBase = mode ? (await gateRange(absolute, mode, options.base))?.base ?? null
|
|
443
|
+
: since ? (await git(absolute, ["rev-parse", "--verify", "--quiet", `${since}^{commit}`])).out || null : null;
|
|
440
444
|
// At the gate, like records, a stray the change does not touch was there when it merged and is not judged again.
|
|
441
445
|
// A data file belongs only as a report some record in its tranche names; an unnamed one is authored content by another name.
|
|
442
446
|
// Per tranche: a data file is a report only when a record in its own tranche names it.
|
package/src/hooks.ts
CHANGED
|
@@ -36,13 +36,17 @@ export type WorktreeLayout = { primary: string; worktreeRoot: string; current: s
|
|
|
36
36
|
async function layout(root: string, cfg: WorktreePolicy): Promise<WorktreeLayout | { error: string }> { const current = await realpath(resolve(root)), commonRoot = await primaryRoot(current); if (!commonRoot) return { error: "not inside a Git worktree" }; const primary = await realpath(commonRoot), expectedPrimary = join(dirname(primary), cfg.primary_directory); if (primary !== expectedPrimary) return { error: `primary checkout must be ${expectedPrimary}, not ${primary}` }; const branch = (await git(primary, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out; if (branch !== cfg.primary_branch) return { error: `primary checkout ${primary} must be on ${cfg.primary_branch}, not ${branch || "a detached HEAD"}` }; return { primary, worktreeRoot: resolve(primary, cfg.root), current, branch: (await git(current, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out }; }
|
|
37
37
|
async function worktreeCommitPolicy(root: string, cfg: WorktreePolicy): Promise<string | null> { if (!cfg.enabled || !cfg.require_linked_worktree) return null; const state = await layout(root, cfg); if ("error" in state) return state.error; if (state.current === state.primary) return `commits must use a linked worktree beneath ${state.worktreeRoot}; run: atdd-bun worktree start <branch>`; if (!inside(state.worktreeRoot, state.current)) return `linked worktree ${state.current} is outside required root ${state.worktreeRoot}`; if (!state.branch) return "commits require a named branch, not a detached HEAD"; return null; }
|
|
38
38
|
|
|
39
|
+
/** Where the pushed branch left its base branch. A delivery record unchanged since then was judged when it merged, so
|
|
40
|
+
* pre-push does not judge it again, exactly as the merge gate does not; with no base found, every record is judged. */
|
|
41
|
+
async function pushBase(root: string, remote: string, cfg: HookPolicy): Promise<string | null> { for (const ref of [`${remote}/HEAD`, ...cfg.protected_branches.map(name => `${remote}/${name}`)]) { const found = await git(root, ["merge-base", "HEAD", ref]); if (!found.code && found.out) return found.out; } return null; }
|
|
42
|
+
|
|
39
43
|
export async function runHook(event: HookEvent, root = process.cwd(), args: string[] = [], stdin = "") {
|
|
40
44
|
const cfg = await policy(root), branch = (await git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out;
|
|
41
45
|
if (["pre-commit", "pre-merge-commit"].includes(event) && cfg.protected_branches.includes(branch)) return bad(`protected branch ${branch} is blocked`);
|
|
42
46
|
if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
|
|
43
47
|
if (event === "pre-commit") { const staged = await files(root, ["diff", "--cached", "--name-only"]), registries = staged.filter(path => isRegistry(cfg, path)), generated = staged.filter(isGeneratedView), counted = staged.length - registries.length - generated.length, lines = (await stagedLineStats(root)).filter(row => !isRegistry(cfg, row.path) && !isGeneratedView(row.path)).reduce((n, row) => n + row.added + row.removed, 0); if (counted > cfg.max_staged_files) return bad(`staged files ${counted} exceed ${cfg.max_staged_files}`); if (lines > cfg.max_staged_changed_lines) return bad(`staged changed lines ${lines} exceed ${cfg.max_staged_changed_lines}`); return cfg.require_traceability || registries.length || generated.length ? validation(root, staged, false, registries.length > 0) : { ok: true, message: "ok" }; }
|
|
44
48
|
if (event === "commit-msg") { const deleted = (await files(root, ["diff", "--cached", "--name-only", "--diff-filter=D"])).length, stats = await stagedStats(root), lines = stats.reduce((n, row) => n + row.removed, 0), registryRemoved = stats.filter(row => isRegistry(cfg, row.path)).reduce((n, row) => n + row.removed - row.added, 0), message = args[0] && existsSync(args[0]) ? await readFile(args[0], "utf8") : "", approved = message.includes("[mass-delete-approved]"); if ((deleted > 50 || lines > 10_000) && !approved) return bad("mass delete requires [mass-delete-approved]"); return registryRemoved > cfg.max_registry_removed_lines && !approved ? bad(`registry removal of ${registryRemoved} net lines exceeds ${cfg.max_registry_removed_lines}; requires [mass-delete-approved]`) : { ok: true, message: "ok" }; }
|
|
45
|
-
if (event === "pre-push") { for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [,,remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); const local = row[1]; if (local && !/^0+$/.test(local)) { const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, count = Number((await git(root, ["rev-list", "--count", range])).out); if (count > cfg.max_commits_per_push) return bad(`commits per push ${count} exceed ${cfg.max_commits_per_push}`); } } return validation(root, await files(root, ["diff", "--name-only", "HEAD~1..HEAD"]), true); }
|
|
49
|
+
if (event === "pre-push") { for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [,,remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); const local = row[1]; if (local && !/^0+$/.test(local)) { const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, count = Number((await git(root, ["rev-list", "--count", range])).out); if (count > cfg.max_commits_per_push) return bad(`commits per push ${count} exceed ${cfg.max_commits_per_push}`); } } const base = await pushBase(root, args[0] || "origin", cfg), previous = process.env.ATDD_DELIVERY_SINCE; if (base) process.env.ATDD_DELIVERY_SINCE = base; else delete process.env.ATDD_DELIVERY_SINCE; try { return await validation(root, await files(root, ["diff", "--name-only", "HEAD~1..HEAD"]), true); } finally { if (previous === undefined) delete process.env.ATDD_DELIVERY_SINCE; else process.env.ATDD_DELIVERY_SINCE = previous; } }
|
|
46
50
|
if (event === "post-commit") { const result = await validation(root, await files(root, ["show", "--pretty=format:", "--name-only", "HEAD"]), false); return { ok: true, message: result.ok ? result.message : `advisory: ${result.message}` }; }
|
|
47
51
|
return { ok: true, message: "ok" };
|
|
48
52
|
}
|