@afokapu/atdd-bun 0.10.0 → 0.10.2
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
CHANGED
|
@@ -156,6 +156,10 @@ recorded work. Independence now defaults to `different-model`: a repository that
|
|
|
156
156
|
`fresh-process` default sets it explicitly. A repository with no `stages:` gets the new default
|
|
157
157
|
operating model, without the integrity check reporting the change.
|
|
158
158
|
|
|
159
|
+
From 0.10.2 the pre-commit hook no longer counts uncommitted work: it refused exactly the small
|
|
160
|
+
commits that reduce it. `max_uncommitted_files` is no longer read; commit size stays limited by
|
|
161
|
+
`max_staged_files` and `max_staged_changed_lines`.
|
|
162
|
+
|
|
159
163
|
Every key is optional; these are the defaults:
|
|
160
164
|
|
|
161
165
|
```yaml
|
|
@@ -4,23 +4,23 @@ kind: policy
|
|
|
4
4
|
status: active
|
|
5
5
|
name: What each review establishes, and how a reviewer works
|
|
6
6
|
statement: >-
|
|
7
|
-
A reviewer is a fresh, read-only process for one stage of one tranche, given the stage, the base SHA, the SHA and the RED commit. It reviews the change between the base and the SHA, working through its stage's checklist systematically, systemically and
|
|
7
|
+
A reviewer is a fresh, read-only process for one stage of one tranche, given the stage, the base SHA, the SHA and the RED commit. It reviews the change between the base and the SHA, working through its stage's checklist systematically, systemically, adversarially and economically, and returns one review entry with verdict, checked and findings.
|
|
8
8
|
terms:
|
|
9
9
|
- term_id: plan
|
|
10
10
|
text: >-
|
|
11
|
-
Are we building the right thing? The plan files the change adds or modifies: the decomposition (wagon, WMBT, acceptance, train, journey, contract) covers the intent; every acceptance is testable with one observable outcome and every WMBT has a SMOKE acceptance; edge cases, migrations, security and architecture constraints are represented; the acceptances can prove the feature; owned files do not overlap other tranches.
|
|
11
|
+
Are we building the right thing? The plan files the change adds or modifies: the decomposition (wagon, WMBT, acceptance, train, journey, contract) covers the intent; every acceptance is testable with one observable outcome and every WMBT has a SMOKE acceptance; edge cases, migrations, security and architecture constraints are represented; the acceptances can prove the feature; the plan builds on the wagons, contracts and components that already exist wherever they fit, and plans no machinery outside the tranche's scope; it follows the accepted architecture decisions (docs/architecture/decisions/index.adoc), and a new technology choice comes with its own ADR; owned files do not overlap other tranches.
|
|
12
12
|
- term_id: final
|
|
13
13
|
text: >-
|
|
14
|
-
Does the change satisfy the approved plan? The tests, implementation and refactoring the change brings are read together. Every acceptance the plan names has a test bound by URN that asserts the behaviour, not a mock; each acceptance test differs from its RED commit only by the removed RED marker and Phase header, and any other change is justified; behaviour is correct on edge and error paths; the changed code keeps layering, composition, DTO, error-response and security sound; SMOKE runs through the real entry point; nothing drifts from the plan.
|
|
14
|
+
Does the change satisfy the approved plan? The tests, implementation and refactoring the change brings are read together. Every acceptance the plan names has a test bound by URN that asserts the behaviour, not a mock; each acceptance test differs from its RED commit only by the removed RED marker and Phase header, and any other change is justified; behaviour is correct on edge and error paths; the changed code keeps layering, composition, DTO, error-response and security sound; SMOKE runs through the real entry point; the change reuses what the codebase already has, duplicates nothing, and adds no machinery beyond what the plan needs; it uses what the accepted ADRs chose and brings in no alternative to them; nothing drifts from the plan.
|
|
15
15
|
- term_id: red
|
|
16
16
|
text: >-
|
|
17
17
|
Where the policy gives red a reviewer. Every acceptance has a RED test bound by URN that fails for the missing behaviour and would still fail for a wrong implementation, asserting observable output rather than mocks.
|
|
18
18
|
- term_id: green_refactor
|
|
19
19
|
text: >-
|
|
20
|
-
Where the policy gives green or refactor a reviewer. Every behaviour is correct against its acceptance on edge and error paths; the changed code keeps layering, composition, DTO, error-response and security sound; SMOKE runs through the real entry point.
|
|
20
|
+
Where the policy gives green or refactor a reviewer. Every behaviour is correct against its acceptance on edge and error paths; the changed code keeps layering, composition, DTO, error-response and security sound, reuses what exists and duplicates nothing; SMOKE runs through the real entry point.
|
|
21
21
|
- term_id: method
|
|
22
22
|
text: >-
|
|
23
|
-
Systematic, the whole checklist, with everything checked listed in checked, not only what failed. Systemic, from the change to what it affects (callers, contracts, other tranches, downstream owners), named in affects; code the change does not touch or affect is out of scope. Adversarial, assuming the change is wrong and proving it; a finding needs a file and line, a failing command or a counter-example. A small change gets a short review.
|
|
23
|
+
Systematic, the whole checklist, with everything checked listed in checked, not only what failed. Systemic, from the change to what it affects (callers, contracts, other tranches, downstream owners), named in affects; code the change does not touch or affect is out of scope. Adversarial, assuming the change is wrong and proving it; a finding needs a file and line, a failing command or a counter-example. Economical, the change does its job with the least new machinery: it reuses what exists, duplicates nothing, and adds no framework, abstraction, configuration, tooling or meta machinery (machinery for building or checking machinery) beyond the tranche's scope; such a finding names the existing piece to reuse or the part to remove. Economy never trades away effectiveness: a smaller change that no longer satisfies an acceptance is not an improvement. A small change gets a short review.
|
|
24
24
|
content:
|
|
25
25
|
summary: >-
|
|
26
26
|
The two default reviews carry the judgement no gate can: the plan before RED, and the whole change at its head. The gates between them are deterministic.
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.2",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "763767126dd67f0f0ed0a3955cc6c707370f79b60739cab05773f41b1d37fa97",
|
|
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",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"conventions/delivery/delivery.merge-gate.convention.yaml": "48745ec23625b68be9b8fe2c6dcdcba88b2b38924f6593abf6987391ff0ee243",
|
|
100
100
|
"conventions/delivery/delivery.model-allowed.convention.yaml": "1071a727f61bacd21d70921885826974234ef0a58c0a814454b9375a0432b0fe",
|
|
101
101
|
"conventions/delivery/delivery.operating-model.convention.yaml": "980858b735974093c98e850d8f6dd83cb21eea86a8b6e8249de678f0fc336f1b",
|
|
102
|
-
"conventions/delivery/delivery.review.convention.yaml": "
|
|
102
|
+
"conventions/delivery/delivery.review.convention.yaml": "d71b889a92ca695bc7bc989636ef16272860a4d3389a2f1e2cf3776257e97af8",
|
|
103
103
|
"conventions/delivery/delivery.reviewer-independent.convention.yaml": "264df63e768a2a17dca782dddbba6ba47a21fa3dd9cd39ed150f9a00dd04bdb0",
|
|
104
104
|
"conventions/delivery/delivery.stages-complete.convention.yaml": "2658cb9f4a4cd8939231e4ebb0ed4db2d0982c94690d25e4577de9dad4c2f742",
|
|
105
105
|
"conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
|
|
@@ -1198,9 +1198,9 @@
|
|
|
1198
1198
|
"src/delivery.ts": "f78cb15e1eabeca1492bd8f57b2029370d9c2dbc3d3fb7d25ed3a18ea9de87cb",
|
|
1199
1199
|
"src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
|
|
1200
1200
|
"src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
|
|
1201
|
-
"src/hooks.ts": "
|
|
1201
|
+
"src/hooks.ts": "9779eb48f9d0cb6234c33f6616f98dde6583d2ee03ac3c19f3ace2d156af26d0",
|
|
1202
1202
|
"src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
|
|
1203
|
-
"src/integrity.ts": "
|
|
1203
|
+
"src/integrity.ts": "ba3de29e12e62a1b3e0149e243183fdfbce33d88efc9d39605b54996c01426cd",
|
|
1204
1204
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
1205
1205
|
"src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
|
|
1206
1206
|
"src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
|
|
@@ -1212,7 +1212,7 @@
|
|
|
1212
1212
|
"src/topology.ts": "9779ee48e1c453abb76363163d4ab363463fa14e359571139f6d1f1fcc51ae2c",
|
|
1213
1213
|
"templates/agents/AGENTS.block.md": "7e9687b1eff245b66da4127b336a9e20af2f5fa273e08895188c0a0dffebd77d",
|
|
1214
1214
|
"templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
|
|
1215
|
-
"templates/agents/atdd/SKILL.md": "
|
|
1215
|
+
"templates/agents/atdd/SKILL.md": "8f36d7d571aa17beb03df5e620ff837be73eb21a4e73dabd2c12188d2b378022",
|
|
1216
1216
|
"templates/agents/delivery/SKILL.md": "0c4fa25303ada52db9c6b67d51a4011c15039e92c4f2947994ec9cba4d0c5efa",
|
|
1217
1217
|
"templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
|
|
1218
1218
|
"templates/github/atdd-bun.yml": "bb42414f72f4b9a2fb1eb69530695e193f105a984542f4ca124c898a5f1489be"
|
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { topologyFor } from "./topology";
|
|
|
6
6
|
import { JOURNEY_DOCS_DIR } from "./journey-docs";
|
|
7
7
|
|
|
8
8
|
export type WorktreePolicy = { enabled: boolean; root: string; primary_directory: string; primary_branch: string; require_linked_worktree: boolean };
|
|
9
|
-
export type HookPolicy = { max_staged_files: number; max_staged_changed_lines: number;
|
|
9
|
+
export type HookPolicy = { max_staged_files: number; max_staged_changed_lines: number; max_commits_per_push: number; max_registry_removed_lines: number; registry_paths: string[]; protected_branches: string[]; require_plan_reference: boolean; require_traceability: boolean; worktrees: WorktreePolicy };
|
|
10
10
|
export const defaultWorktreePolicy: WorktreePolicy = { enabled: false, root: "../worktrees", primary_directory: "main", primary_branch: "main", require_linked_worktree: true };
|
|
11
|
-
export const defaultHookPolicy: HookPolicy = { max_staged_files: 20, max_staged_changed_lines: 350,
|
|
11
|
+
export const defaultHookPolicy: HookPolicy = { max_staged_files: 20, max_staged_changed_lines: 350, max_commits_per_push: 10, max_registry_removed_lines: 350, registry_paths: ["plan/_*.yaml", "plan/_*.yml", "contracts/_*.yaml", "contracts/_*.yml"], protected_branches: ["main", "master"], require_plan_reference: true, require_traceability: true, worktrees: defaultWorktreePolicy };
|
|
12
12
|
export const hookEvents = ["pre-commit", "commit-msg", "pre-push", "pre-merge-commit", "post-commit", "post-merge"] as const;
|
|
13
13
|
export type HookEvent = typeof hookEvents[number];
|
|
14
14
|
const git = async (root: string, args: string[], input?: string) => { const child = Bun.spawn({ cmd: ["git", ...args], cwd: root, stdin: input ? new Blob([input]) : undefined, stdout: "pipe", stderr: "pipe" }); return { code: await child.exited, out: (await new Response(child.stdout).text()).trim(), err: (await new Response(child.stderr).text()).trim() }; };
|
|
@@ -40,7 +40,7 @@ export async function runHook(event: HookEvent, root = process.cwd(), args: stri
|
|
|
40
40
|
const cfg = await policy(root), branch = (await git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out;
|
|
41
41
|
if (["pre-commit", "pre-merge-commit"].includes(event) && cfg.protected_branches.includes(branch)) return bad(`protected branch ${branch} is blocked`);
|
|
42
42
|
if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
|
|
43
|
-
if (event === "pre-commit") { const staged = await files(root, ["diff", "--cached", "--name-only"]),
|
|
43
|
+
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
44
|
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
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); }
|
|
46
46
|
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}` }; }
|
package/src/integrity.ts
CHANGED
|
@@ -132,7 +132,7 @@ function withoutNulls(config: Record<string, unknown>): Record<string, unknown>
|
|
|
132
132
|
* where a list is expected) or compare them as the defaults. An empty or null document is the default policy, and fine. */
|
|
133
133
|
export function policyShapeErrors(config: Record<string, unknown>): string[] {
|
|
134
134
|
const out: string[] = [];
|
|
135
|
-
for (const key of ["max_staged_files", "max_staged_changed_lines", "
|
|
135
|
+
for (const key of ["max_staged_files", "max_staged_changed_lines", "max_commits_per_push", "max_registry_removed_lines"])
|
|
136
136
|
if (config[key] !== undefined && !(typeof config[key] === "number" && Number.isFinite(config[key]))) out.push(`${key} must be a finite number`);
|
|
137
137
|
for (const key of ["require_plan_reference", "require_traceability"]) if (config[key] !== undefined && typeof config[key] !== "boolean") out.push(`${key} must be true or false`);
|
|
138
138
|
for (const key of ["protected_branches", "registry_paths"]) if (config[key] !== undefined && !(Array.isArray(config[key]) && (config[key] as unknown[]).every(item => typeof item === "string"))) out.push(`${key} must be a list of strings`);
|
|
@@ -152,7 +152,7 @@ export function policyShapeErrors(config: Record<string, unknown>): string[] {
|
|
|
152
152
|
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown; delivery?: unknown }, current: Partial<HookPolicy> & { profiles?: unknown; delivery?: unknown }): string[] {
|
|
153
153
|
const b = { ...defaultHookPolicy, ...base, worktrees: { ...defaultHookPolicy.worktrees, ...base.worktrees } }, c = { ...defaultHookPolicy, ...current, worktrees: { ...defaultHookPolicy.worktrees, ...current.worktrees } };
|
|
154
154
|
const out: string[] = [];
|
|
155
|
-
for (const key of ["max_staged_files", "max_staged_changed_lines", "
|
|
155
|
+
for (const key of ["max_staged_files", "max_staged_changed_lines", "max_commits_per_push", "max_registry_removed_lines"] as const) if (Number(c[key]) > Number(b[key])) out.push(`${key} ${b[key]} → ${c[key]}`);
|
|
156
156
|
for (const key of ["require_plan_reference", "require_traceability"] as const) if (b[key] && !c[key]) out.push(`${key} true → false`);
|
|
157
157
|
for (const key of ["enabled", "require_linked_worktree"] as const) if (b.worktrees[key] && !c.worktrees[key]) out.push(`worktrees.${key} true → false`);
|
|
158
158
|
const removed = b.protected_branches.filter(x => !c.protected_branches.includes(x)), added = c.registry_paths.filter(x => !b.registry_paths.includes(x));
|
|
@@ -6,6 +6,8 @@ description: Use before writing or changing code, tests, or plan/ files in this
|
|
|
6
6
|
|
|
7
7
|
Conventions live in `node_modules/@afokapu/atdd-bun/` (`planner-nodes/nodes/`, `conventions/`). Read the ones a stage names; do not restate them. Finish each stage by passing its gate before starting the next.
|
|
8
8
|
|
|
9
|
+
Technology and architecture choices already made are the accepted ADRs listed in `docs/architecture/decisions/index.adoc`. Follow them in PLAN and GREEN; a new choice gets its own ADR under `docs/architecture/decisions/` (`planner.docs.*`).
|
|
10
|
+
|
|
9
11
|
1. PLAN — Decompose the intent into wagon → WMBT → acceptance → train/interlocking → journey → contract under `plan/`, following `planner.decomposition.*`; every WMBT declares a SMOKE acceptance. Gate: `bun run atdd-bun planner`.
|
|
10
12
|
2. RED — For each acceptance, write a test headed `// URN: test:{wagon}:{feature}:{ACC-ID}` and `// Phase: RED` that fails for the missing behaviour (`tester.bun.red-*`). Gate: `bun run atdd-bun tester`.
|
|
11
13
|
3. GREEN — Write the least code that passes; each source file carries `URN: component:{wagon}:{feature}:{Name}:{side}:{layer}` and a `Tested-By:` block (`coder.bun.green-*`). Gate: `bun test`.
|