@afokapu/atdd-bun 0.10.61 → 0.10.62
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 +23 -6
- package/integrity.json +4 -4
- package/package.json +1 -1
- package/src/hooks.ts +10 -6
- package/src/setup.ts +19 -5
package/README.md
CHANGED
|
@@ -103,8 +103,11 @@ the adoption that establishes the governed set, so a brownfield repository can d
|
|
|
103
103
|
`profiles: [docs]` in an ordinary pull request. From then on, the integrity check reports, against
|
|
104
104
|
the base branch, removing a profile from the list and removing the list itself. The second closes
|
|
105
105
|
the two-step bypass `[docs, security]` → no list → `[docs]`. `init` writes a new `atdd-bun.yaml`
|
|
106
|
-
with every profile listed
|
|
107
|
-
|
|
106
|
+
with every profile listed and a matching explicit `reject-new` ratchet policy, so a greenfield
|
|
107
|
+
repository is governed from its first commit. For an existing configuration with an explicit
|
|
108
|
+
profile list but no policy, `init` appends only the matching policy and preserves the selected
|
|
109
|
+
profiles and every other configuration field; keep the two selected-profile lists equal when
|
|
110
|
+
trimming them before the first commit to adopt gradually.
|
|
108
111
|
|
|
109
112
|
## Configuration
|
|
110
113
|
|
|
@@ -129,9 +132,13 @@ release: { enabled: false }
|
|
|
129
132
|
|
|
130
133
|
When enabled without overrides, a primary checkout at `~/Github/<repo>` uses linked worktrees at `~/Github/worktrees/<repo>/<branch>`. The defaults use `primary_directory: .` (the current primary checkout) and `root: ../worktrees/{repo}`; `{repo}` expands to the primary checkout's directory name. Repositories that need another layout can set either field explicitly.
|
|
131
134
|
|
|
132
|
-
## Exact-base finding ratchet
|
|
135
|
+
## Exact-base finding ratchet
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
`bun run atdd-bun init` generates an explicit `reject-new` policy whose profiles exactly match
|
|
138
|
+
the generated selected profiles. For an existing explicit selection with no policy, the same
|
|
139
|
+
command appends that matching policy without changing the selection or other configuration.
|
|
140
|
+
Without a policy, strict enforcement remains the default; `bun run atdd-bun all` still fails for
|
|
141
|
+
every finding:
|
|
135
142
|
|
|
136
143
|
```yaml
|
|
137
144
|
# atdd-bun.yaml
|
|
@@ -160,9 +167,19 @@ bun run atdd-bun --profile flow,traceability --ratchet --ratchet-activate-profil
|
|
|
160
167
|
|
|
161
168
|
This is an activation-only exception to the ordinary equal-context rule, not a baseline migration. It requires an explicit candidate `profiles:` list, an explicit base list that is a strict subset of it, and identical non-profile policy/configuration. A removal, implicit/default-list adoption, unrelated configuration change, dirty tree, unavailable/non-ancestor base, or omission of `--ratchet-activate-profiles` fails closed. The command stores no activation state or accepted findings; after the governed expansion lands, use ordinary `--ratchet` with exact matching context. Strict non-ratchet enforcement remains unchanged.
|
|
162
169
|
|
|
163
|
-
### Generated
|
|
170
|
+
### Generated exact-base commit gate
|
|
164
171
|
|
|
165
|
-
A generated `pre-commit` hook recognizes
|
|
172
|
+
A generated `pre-commit` hook recognizes a staged `reject-new` policy and materializes the exact
|
|
173
|
+
committed `HEAD` base plus the staged index tree in an isolated worktree. With an already-active,
|
|
174
|
+
unchanged explicit profile set and identical context, it runs ordinary exact-base comparison and
|
|
175
|
+
emits the carried/new/resolved report; the commit proceeds only when `new` is empty. A strict
|
|
176
|
+
profile expansion uses the same comparison as the one-time activation exception.
|
|
177
|
+
|
|
178
|
+
The gate never writes a baseline or weakens policy outside that exact comparison. It fails closed
|
|
179
|
+
unless the index has the explicit matching ratchet policy, the worktree has no unstaged or
|
|
180
|
+
untracked state, and either the selected profiles/context are unchanged or the base profiles are
|
|
181
|
+
a strict subset with identical non-profile policy. Removals, context drift, unavailable/non-ancestor
|
|
182
|
+
bases, a bypass, waivers, mutable baselines, and new findings remain rejected.
|
|
166
183
|
|
|
167
184
|
## Docs-site theme
|
|
168
185
|
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.62",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "207414ffc3947dcdc5fc778df63f3e870a5b6cca283e68751e439eed2c03fb0a",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/_profiles/architecture.yaml": "f1808768b720a4e476047e4a0912002ca673946562cea9aeef5d0c214828d93a",
|
|
9
9
|
"conventions/_profiles/coder.yaml": "3fb191556a64c9ebcf249516afa189afca24da9b22fd40b2fc66908196cdfb78",
|
|
@@ -1261,7 +1261,7 @@
|
|
|
1261
1261
|
"src/cli.ts": "90e59edbb8a7ea116ad9e7b4ba56b5c612ac18ada05824e8e941048be4dc72c8",
|
|
1262
1262
|
"src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
|
|
1263
1263
|
"src/enforce.ts": "7e6b3a1fc7c90f8e1836ab3e4655778fb569a63be5e7c199de44024fc003e6be",
|
|
1264
|
-
"src/hooks.ts": "
|
|
1264
|
+
"src/hooks.ts": "4816318553e8f6a96716d8f72f6a1976734fe8333ac4f1d2d8144ac71fefad81",
|
|
1265
1265
|
"src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
|
|
1266
1266
|
"src/integrity.ts": "f5ad22759eec675454df82cdcf791b1f081951f08ca5084ebdcb8cd82aa1ecef",
|
|
1267
1267
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
@@ -1272,7 +1272,7 @@
|
|
|
1272
1272
|
"src/ratchet.ts": "801038b182615ed883197e0e29c98da9ac5f81bf4db4eb36b989065e32c99b6b",
|
|
1273
1273
|
"src/register.ts": "6a2f1c6b8df0dc9c8f45df8ec9fac2be29044e048b08c8ffe423410e94dadedd",
|
|
1274
1274
|
"src/release.ts": "7b0fe444ee81f476f53b9c21b6c347ef588ee616c60211419cf984132be768ba",
|
|
1275
|
-
"src/setup.ts": "
|
|
1275
|
+
"src/setup.ts": "c6804f699808c01a849ea46489b0e9feab7991da4cb62a29edd439423e0a88c0",
|
|
1276
1276
|
"src/telemetry-plan.ts": "cdb1e8bfff414f3ea81977388973da2cf6d89567d9e060dc9d5989a33600f153",
|
|
1277
1277
|
"src/topology.ts": "9779ee48e1c453abb76363163d4ab363463fa14e359571139f6d1f1fcc51ae2c",
|
|
1278
1278
|
"templates/agents/AGENTS.block.md": "84d19dfabedb1e05ccfb7b19a38644e87f193ae2b74755897f75f13971bb687e",
|
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -40,8 +40,8 @@ const configAt = async (root: string) => {
|
|
|
40
40
|
return existsSync(file) ? Bun.YAML.parse(await readFile(file, "utf8")) as Record<string, unknown> : {};
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
async function
|
|
43
|
+
/** A staged reject-new policy compares its exact HEAD: equal active profiles are ordinary ratchet flow; strict expansion is the one bootstrap exception. */
|
|
44
|
+
async function ratchetCommitGate(root: string): Promise<{ ok: boolean; message: string } | null> {
|
|
45
45
|
const stagedConfig = await git(root, ["show", ":atdd-bun.yaml"]);
|
|
46
46
|
if (stagedConfig.code) return null;
|
|
47
47
|
let candidateConfig: Record<string, unknown>, policy;
|
|
@@ -59,14 +59,18 @@ async function profileActivationCommitGate(root: string): Promise<{ ok: boolean;
|
|
|
59
59
|
if (checkedOut.code) return bad(`could not checkout ratchet activation base: ${checkedOut.err || checkedOut.out}`);
|
|
60
60
|
const baseConfig = await configAt(scratch), candidateProfiles = stable(candidateConfig.profiles.map(normalizeProfileName));
|
|
61
61
|
const baseProfiles = stable(await enabledProfiles(scratch));
|
|
62
|
-
|
|
62
|
+
const policyMatches = stable(policy.profiles.map(normalizeProfileName)).join(",") === candidateProfiles.join(",");
|
|
63
|
+
const activation = isStrictProfileExpansion(baseProfiles, candidateProfiles) && activationContextDigest(baseConfig) === activationContextDigest(candidateConfig);
|
|
64
|
+
const unchanged = baseProfiles.join(",") === candidateProfiles.join(",") && contextDigest(baseConfig, baseProfiles) === contextDigest(candidateConfig, candidateProfiles);
|
|
65
|
+
if (candidateProfiles.some(profile => !concreteProfiles.includes(profile as typeof concreteProfiles[number])) || !policyMatches || (!activation && !unchanged)) return bad("ratchet reject-new requires either unchanged explicit profiles/context or an explicit monotonic profile expansion with identical non-profile context");
|
|
63
66
|
const baseFindings = await enforce({ root: scratch, profiles: candidateProfiles as any });
|
|
64
67
|
const patch = await git(root, ["diff", "--cached", "--binary"], undefined, true);
|
|
65
68
|
if (patch.code) return bad(`could not read staged ratchet activation candidate: ${patch.err || patch.out}`);
|
|
66
69
|
const applied = await git(scratch, ["apply", "--whitespace=nowarn"], patch.out);
|
|
67
70
|
if (applied.code) return bad(`could not materialize staged ratchet activation candidate: ${applied.err || applied.out}`);
|
|
68
|
-
const materialized = await configAt(scratch);
|
|
69
|
-
|
|
71
|
+
const materialized = await configAt(scratch), materializedProfiles = stable(await enabledProfiles(scratch));
|
|
72
|
+
const materializedContext = activation ? activationContextDigest(materialized) === activationContextDigest(candidateConfig) : materializedProfiles.join(",") === candidateProfiles.join(",") && contextDigest(materialized, materializedProfiles) === contextDigest(candidateConfig, candidateProfiles);
|
|
73
|
+
if (!materializedContext) return bad("staged ratchet candidate did not materialize exactly");
|
|
70
74
|
const candidateFindings = await enforce({ root: scratch, profiles: candidateProfiles as any });
|
|
71
75
|
const normalize = (findings: Awaited<ReturnType<typeof enforce>>) => findings.map(finding => ({ ...finding, file: canonicalFindingPath(scratch, finding.file) }));
|
|
72
76
|
const delta = compareFindings(normalize(baseFindings), normalize(candidateFindings));
|
|
@@ -88,7 +92,7 @@ export async function runHook(event: HookEvent, root = process.cwd(), args: stri
|
|
|
88
92
|
const cfg = await policy(root), branch = (await git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"])).out;
|
|
89
93
|
if (["pre-commit", "pre-merge-commit"].includes(event) && cfg.protected_branches.includes(branch)) return bad(`protected branch ${branch} is blocked`);
|
|
90
94
|
if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
|
|
91
|
-
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}`); const
|
|
95
|
+
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}`); const ratchet = await ratchetCommitGate(root); if (ratchet) return ratchet; return cfg.require_traceability || registries.length || generated.length ? validation(root, staged, false, registries.length > 0) : { ok: true, message: "ok" }; }
|
|
92
96
|
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" }; }
|
|
93
97
|
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); }
|
|
94
98
|
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/setup.ts
CHANGED
|
@@ -2,19 +2,33 @@ import { ciInit, ciStatus } from "./ci";
|
|
|
2
2
|
import { hooksStatus, installHooks } from "./hooks";
|
|
3
3
|
import { agentInit, agentStatus } from "./agent";
|
|
4
4
|
import { integrityInit, integrityStatus } from "./integrity";
|
|
5
|
-
import { defaultProfiles } from "./enforce";
|
|
5
|
+
import { concreteProfiles, defaultProfiles, normalizeProfileName } from "./enforce";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { writeFile } from "node:fs/promises";
|
|
7
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
/** A new atdd-bun.yaml declares every profile explicitly. An absent list runs every profile but governs none, so
|
|
11
11
|
* without this a greenfield repository's first `profiles:` line could switch most of them off unreported. A
|
|
12
12
|
* brownfield repository trims the list before its first commit; after that, dropping one is a reported loosening.
|
|
13
|
-
* An existing
|
|
13
|
+
* An existing explicit profile list gains only its matching generated reject-new policy; all other existing policy
|
|
14
|
+
* remains untouched. */
|
|
14
15
|
export async function policyInit(root = process.cwd()) {
|
|
15
16
|
const file = join(root, "atdd-bun.yaml");
|
|
16
|
-
if (existsSync(file))
|
|
17
|
-
|
|
17
|
+
if (existsSync(file)) {
|
|
18
|
+
const text = await readFile(file, "utf8");
|
|
19
|
+
try {
|
|
20
|
+
const config = Bun.YAML.parse(text) as Record<string, unknown>;
|
|
21
|
+
const profiles = config && typeof config === "object" && !Array.isArray(config) && Array.isArray(config.profiles) && config.profiles.length && config.profiles.every(profile => typeof profile === "string") ? config.profiles as string[] : null;
|
|
22
|
+
const normalized = profiles?.map(normalizeProfileName);
|
|
23
|
+
const validProfiles = normalized && normalized.every(profile => concreteProfiles.includes(profile as typeof concreteProfiles[number])) && new Set(normalized).size === normalized.length;
|
|
24
|
+
if (profiles && validProfiles && config.ratchet === undefined) {
|
|
25
|
+
await writeFile(file, `${text.replace(/\s*$/, "")}\nratchet:\n mode: reject-new\n profiles: ${JSON.stringify(profiles)}\n`);
|
|
26
|
+
return { ok: true, message: `${file} added ratchet policy` };
|
|
27
|
+
}
|
|
28
|
+
} catch { /* Preserve malformed and non-policy consumer configuration for its normal validation path. */ }
|
|
29
|
+
return { ok: true, message: `${file} kept` };
|
|
30
|
+
}
|
|
31
|
+
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${defaultProfiles.join(", ")}]\nratchet:\n mode: reject-new\n profiles: [${defaultProfiles.join(", ")}]\n`);
|
|
18
32
|
return { ok: true, message: file };
|
|
19
33
|
}
|
|
20
34
|
|