@afokapu/atdd-bun 0.10.50 → 0.10.51
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 +21 -0
- package/integrity.json +4 -3
- package/package.json +1 -1
- package/src/cli.ts +60 -8
- package/src/ratchet.ts +54 -0
package/README.md
CHANGED
|
@@ -122,6 +122,27 @@ release: { enabled: false }
|
|
|
122
122
|
|
|
123
123
|
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.
|
|
124
124
|
|
|
125
|
+
## Exact-base finding ratchet (opt-in)
|
|
126
|
+
|
|
127
|
+
Strict enforcement is the default: `bun run atdd-bun all` still fails for every finding. A consumer may opt into an exact-base comparison for one explicit profile set:
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# atdd-bun.yaml
|
|
131
|
+
ratchet:
|
|
132
|
+
mode: report # or reject-new
|
|
133
|
+
profiles: [coder, tester]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Run it only against a full reviewed base SHA:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
bun run atdd-bun coder tester --ratchet --base <40-character-reviewed-main-sha>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The command runs the same installed detectors against an isolated exact base and the clean candidate, emits a redacted JSON `carried`/`new`/`resolved` fingerprint report, and never writes or accepts a baseline. `report` records debt without changing command success; `reject-new` exits non-zero only when the candidate adds a finding absent from the equivalent base. The declared `ratchet.profiles` must exactly match the command's effective profiles. Base and candidate profile/context digests must match or comparison fails closed; profile/config adoption is separately governed. Use debt-remediation branches to remove carried findings. Direct implementation profiles can opt in only through this explicit policy and do not alter configured merge profiles or `profiles:`.
|
|
143
|
+
|
|
144
|
+
Fingerprints are SHA-256 of a versioned rule, normalized repository-relative path, and semantic subject where supplied; line/column, rendered evidence, source text, absolute paths, and runtime data are excluded. Ratchet findings are unrelated to Bun `test.failing()` or JUnit/behavioral-test baselines.
|
|
145
|
+
|
|
125
146
|
## Docs-site theme
|
|
126
147
|
|
|
127
148
|
A repository that publishes its AsciiDoc docs as a site does not need its own theme.
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.51",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "16cac8c84aa9b11018b4b1cc2984faa74c06bee1d7ce1360fb234154bbce2d9f",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/_profiles/architecture.yaml": "f1808768b720a4e476047e4a0912002ca673946562cea9aeef5d0c214828d93a",
|
|
9
9
|
"conventions/_profiles/coder.yaml": "3fb191556a64c9ebcf249516afa189afca24da9b22fd40b2fc66908196cdfb78",
|
|
@@ -1195,7 +1195,7 @@
|
|
|
1195
1195
|
"relationships.yaml": "5860a308ebc3ee7fa8172fc2c6633984000ab3003f0ae4b6fa28db48879eda22",
|
|
1196
1196
|
"src/agent.ts": "39ca8b3183063c60e00eb96e5629d0397aeb83cc82d4c4bc32c01715c906649b",
|
|
1197
1197
|
"src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
|
|
1198
|
-
"src/cli.ts": "
|
|
1198
|
+
"src/cli.ts": "8f5d69bdd5940c7213f8b7c342fd94c1dc9387d56dad7458ce6e9286d88181d3",
|
|
1199
1199
|
"src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
|
|
1200
1200
|
"src/enforce.ts": "7e6b3a1fc7c90f8e1836ab3e4655778fb569a63be5e7c199de44024fc003e6be",
|
|
1201
1201
|
"src/hooks.ts": "0b4ca8c857e637bbd144e737e78eda924e3cba7b038c0abe5e6916d200d73068",
|
|
@@ -1206,6 +1206,7 @@
|
|
|
1206
1206
|
"src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
|
|
1207
1207
|
"src/planner-validators.ts": "99adeb65f579af923c11f21a8f55cfb9993b007fd8df59c5f764696b55379cfc",
|
|
1208
1208
|
"src/profile-registry.ts": "5a7761d324181ae27deb51f198d63648a4c99e858b4de98b54d35e6cf8a39fe7",
|
|
1209
|
+
"src/ratchet.ts": "c4ebf34350bc3909db93754f2c92de065325763859bd3040dfaf3d6b204ef33e",
|
|
1209
1210
|
"src/register.ts": "6a2f1c6b8df0dc9c8f45df8ec9fac2be29044e048b08c8ffe423410e94dadedd",
|
|
1210
1211
|
"src/release.ts": "7b0fe444ee81f476f53b9c21b6c347ef588ee616c60211419cf984132be768ba",
|
|
1211
1212
|
"src/setup.ts": "38233265afedc63499bbef3b5403889651add335a70efd3813254ba53f4fc4fe",
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { enforce, flowCompatibilityDiagnostic, normalizeProfileName, profileNames, type Profile } from "./enforce";
|
|
2
|
+
import { enforce, enabledProfiles, flowCompatibilityDiagnostic, normalizeProfileName, profileNames, type Profile } from "./enforce";
|
|
3
|
+
import { compareFindings, contextDigest, parseRatchetPolicy } from "./ratchet";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join, relative, resolve } from "node:path";
|
|
3
8
|
import { finishWorktree, hookEvents, hooksStatus, installHooks, runHook, startWorktree, uninstallHooks, worktreeStatus } from "./hooks";
|
|
4
9
|
import { ciInit, ciStatus } from "./ci";
|
|
5
10
|
import { agentInit, agentStatus } from "./agent";
|
|
@@ -14,7 +19,7 @@ const args = process.argv.slice(2);
|
|
|
14
19
|
const usage = {
|
|
15
20
|
command: "atdd-bun",
|
|
16
21
|
usage: [
|
|
17
|
-
"atdd-bun [profile ...] [--root <path>]",
|
|
22
|
+
"atdd-bun [profile ...] [--root <path>] [--ratchet --base <full-sha>]",
|
|
18
23
|
"atdd-bun init [--replace]",
|
|
19
24
|
"atdd-bun hooks <install|uninstall|status> [--replace]",
|
|
20
25
|
"atdd-bun worktree <start|finish|status>",
|
|
@@ -109,10 +114,11 @@ if (args[0] === "release") {
|
|
|
109
114
|
const valueAfter = (flag: string) => args[args.indexOf(flag) + 1];
|
|
110
115
|
const root = args.includes("--root") ? valueAfter("--root") : process.cwd();
|
|
111
116
|
if (args.includes("--root") && !root) fail("--root requires a path");
|
|
117
|
+
if (args.includes("--base") && !valueAfter("--base")) fail("--base requires a full commit SHA");
|
|
112
118
|
const positional: string[] = [];
|
|
113
119
|
for (let index = 0; index < args.length; index += 1) {
|
|
114
120
|
const arg = args[index];
|
|
115
|
-
if (arg === "--root" || arg === "--profile") { index += 1; continue; }
|
|
121
|
+
if (arg === "--root" || arg === "--profile" || arg === "--base") { index += 1; continue; }
|
|
116
122
|
if (!arg.startsWith("-")) positional.push(arg);
|
|
117
123
|
}
|
|
118
124
|
const profileValue = args.includes("--profile") ? valueAfter("--profile") : undefined;
|
|
@@ -122,9 +128,55 @@ const normalizedRequested = requested.map(normalizeProfileName);
|
|
|
122
128
|
const invalid = normalizedRequested.find(profile => !profileNames.includes(profile as Profile));
|
|
123
129
|
if (invalid) fail("unknown command or profile: " + invalid);
|
|
124
130
|
if (requested.includes("workflow")) console.error(flowCompatibilityDiagnostic);
|
|
125
|
-
const
|
|
126
|
-
for (const violation of violations)
|
|
127
|
-
|
|
131
|
+
const printViolations = (violations: Awaited<ReturnType<typeof enforce>>) => {
|
|
132
|
+
for (const violation of violations) console.error([violation.file, violation.line, violation.col].join(":") + " " + violation.rule_id + " — " + violation.evidence);
|
|
133
|
+
};
|
|
134
|
+
const configAt = async (directory: string) => {
|
|
135
|
+
const file = join(directory, "atdd-bun.yaml");
|
|
136
|
+
return existsSync(file) ? Bun.YAML.parse(await readFile(file, "utf8")) as Record<string, unknown> : {};
|
|
137
|
+
};
|
|
138
|
+
const git = async (directory: string, gitArgs: string[]) => {
|
|
139
|
+
const child = Bun.spawn({ cmd: ["git", ...gitArgs], cwd: directory, stdout: "pipe", stderr: "pipe" });
|
|
140
|
+
return { code: await child.exited, out: (await new Response(child.stdout).text()).trim(), err: (await new Response(child.stderr).text()).trim() };
|
|
141
|
+
};
|
|
142
|
+
const stable = (items: string[]) => [...new Set(items)].sort();
|
|
143
|
+
if (!args.includes("--ratchet")) {
|
|
144
|
+
const violations = await enforce({ root, profiles: normalizedRequested as Profile[] });
|
|
145
|
+
printViolations(violations);
|
|
146
|
+
if (violations.length) console.error("\nUse the rule ID and evidence above to correct the affected artifact. Run atdd-bun help for commands and profiles.");
|
|
147
|
+
process.exitCode = violations.length ? 1 : 0;
|
|
148
|
+
} else {
|
|
149
|
+
const base = valueAfter("--base");
|
|
150
|
+
if (!base || !/^[0-9a-f]{40}$/i.test(base)) fail("--ratchet requires --base <full-40-character-sha>");
|
|
151
|
+
const candidateRoot = resolve(root), candidateConfig = await configAt(candidateRoot), policy = parseRatchetPolicy(candidateConfig);
|
|
152
|
+
if (!policy) fail("--ratchet requires an explicit atdd-bun.yaml ratchet policy");
|
|
153
|
+
const candidateProfiles = normalizedRequested.includes("all") ? await enabledProfiles(candidateRoot) : normalizedRequested;
|
|
154
|
+
if (stable(policy.profiles).join(",") !== stable(candidateProfiles).join(",")) fail("ratchet.profiles must exactly equal the selected profiles");
|
|
155
|
+
if ((await git(candidateRoot, ["status", "--porcelain"])).out) fail("--ratchet requires a porcelain-clean candidate worktree");
|
|
156
|
+
const resolved = await git(candidateRoot, ["rev-parse", "--verify", `${base}^{commit}`]);
|
|
157
|
+
if (resolved.code || resolved.out.toLowerCase() !== base.toLowerCase()) fail(`ratchet base ${base} is unavailable or not a full exact commit`);
|
|
158
|
+
if ((await git(candidateRoot, ["merge-base", "--is-ancestor", base, "HEAD"])).code !== 0) fail("ratchet base must be an ancestor of candidate HEAD");
|
|
159
|
+
const scratch = await mkdtemp(join(tmpdir(), "atdd-ratchet-"));
|
|
160
|
+
try {
|
|
161
|
+
const added = await git(candidateRoot, ["worktree", "add", "--detach", "--no-checkout", scratch, base]);
|
|
162
|
+
if (added.code) { console.error(`could not materialize ratchet base: ${added.err || added.out}`); process.exitCode = 1; }
|
|
163
|
+
else {
|
|
164
|
+
const checkedOut = await git(scratch, ["checkout", "--detach", base]);
|
|
165
|
+
if (checkedOut.code) { console.error(`could not checkout ratchet base: ${checkedOut.err || checkedOut.out}`); process.exitCode = 1; }
|
|
166
|
+
else {
|
|
167
|
+
const baseConfig = await configAt(scratch), baseProfiles = normalizedRequested.includes("all") ? await enabledProfiles(scratch) : normalizedRequested;
|
|
168
|
+
if (stable(baseProfiles).join(",") !== stable(candidateProfiles).join(",") || contextDigest(baseConfig, baseProfiles) !== contextDigest(candidateConfig, candidateProfiles)) { console.error("ratchet base and candidate profile/context differ; use a separately governed adoption path"); process.exitCode = 1; }
|
|
169
|
+
else {
|
|
170
|
+
const [baseFindings, candidateFindings] = await Promise.all([enforce({ root: scratch, profiles: candidateProfiles as Profile[] }), enforce({ root: candidateRoot, profiles: candidateProfiles as Profile[] })]);
|
|
171
|
+
const normalize = (findings: Awaited<ReturnType<typeof enforce>>, directory: string) => findings.map(finding => ({ ...finding, file: relative(directory, finding.file).replaceAll("\\", "/") }));
|
|
172
|
+
const delta = compareFindings(normalize(baseFindings, scratch), normalize(candidateFindings, candidateRoot));
|
|
173
|
+
console.log(JSON.stringify({ schema: "atdd-bun.ratchet-report/v1", mode: policy.mode, base, candidate: (await git(candidateRoot, ["rev-parse", "HEAD"])).out, profiles: stable(candidateProfiles), context: contextDigest(candidateConfig, candidateProfiles), ...delta }, null, 2));
|
|
174
|
+
if (policy.mode === "reject-new" && delta.new.length) process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
await git(candidateRoot, ["worktree", "remove", "--force", scratch]);
|
|
180
|
+
await rm(scratch, { recursive: true, force: true });
|
|
181
|
+
}
|
|
128
182
|
}
|
|
129
|
-
if (violations.length) console.error("\nUse the rule ID and evidence above to correct the affected artifact. Run atdd-bun help for commands and profiles.");
|
|
130
|
-
process.exitCode = violations.length ? 1 : 0;
|
package/src/ratchet.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Pure v1 primitives for the opt-in exact-base finding ratchet. No baseline is written or accepted. */
|
|
2
|
+
export type RatchetMode = "report" | "reject-new";
|
|
3
|
+
export type RatchetPolicy = { mode: RatchetMode; profiles: string[] };
|
|
4
|
+
export type FindingIdentity = {
|
|
5
|
+
rule_id: string;
|
|
6
|
+
file: string;
|
|
7
|
+
subject?: string;
|
|
8
|
+
line?: number;
|
|
9
|
+
col?: number;
|
|
10
|
+
evidence?: string;
|
|
11
|
+
source_line?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const canonical = (value: unknown): string => {
|
|
15
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") return JSON.stringify(value);
|
|
16
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
17
|
+
if (typeof value === "object") return `{${Object.entries(value as Record<string, unknown>).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(",")}}`;
|
|
18
|
+
throw new Error(`cannot canonicalize ${typeof value}`);
|
|
19
|
+
};
|
|
20
|
+
const sha256 = (value: unknown) => new Bun.CryptoHasher("sha256").update(canonical(value)).digest("hex");
|
|
21
|
+
const normalizedPath = (path: string) => {
|
|
22
|
+
const normalized = path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
23
|
+
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || normalized.split("/").includes("..")) throw new Error(`ratchet finding path must be normalized and repository-relative: ${path}`);
|
|
24
|
+
return normalized;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Stable semantic identity: presentation position and rendered evidence deliberately do not participate. */
|
|
28
|
+
export function findingFingerprint(finding: FindingIdentity): string {
|
|
29
|
+
const path = normalizedPath(finding.file), subject = finding.subject ?? path;
|
|
30
|
+
if (!finding.rule_id || !subject) throw new Error("ratchet finding requires rule_id and semantic subject");
|
|
31
|
+
return sha256({ format: "atdd-bun.finding-fingerprint/v1", rule_id: finding.rule_id, path, subject });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The selected profiles and every policy key except ratchet itself must agree across exact base and candidate. */
|
|
35
|
+
export function contextDigest(config: Record<string, unknown>, profiles: string[]): string {
|
|
36
|
+
const { ratchet: _ratchet, ...policy } = config;
|
|
37
|
+
return sha256({ format: "atdd-bun.ratchet-context/v1", profiles: [...profiles].sort(), policy });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseRatchetPolicy(config: Record<string, unknown>): RatchetPolicy | null {
|
|
41
|
+
if (config.ratchet === undefined) return null;
|
|
42
|
+
const value = config.ratchet;
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("atdd-bun.yaml ratchet must be a mapping");
|
|
44
|
+
const mode = (value as Record<string, unknown>).mode, profiles = (value as Record<string, unknown>).profiles;
|
|
45
|
+
if (mode !== "report" && mode !== "reject-new") throw new Error("atdd-bun.yaml ratchet.mode must be report or reject-new");
|
|
46
|
+
if (!Array.isArray(profiles) || !profiles.length || !profiles.every(profile => typeof profile === "string")) throw new Error("atdd-bun.yaml ratchet.profiles must be a non-empty list of profile names");
|
|
47
|
+
return { mode, profiles: [...new Set(profiles)].sort() };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function compareFindings(base: FindingIdentity[], candidate: FindingIdentity[]) {
|
|
51
|
+
const baseSet = new Set(base.map(findingFingerprint)), candidateSet = new Set(candidate.map(findingFingerprint));
|
|
52
|
+
const list = (source: Set<string>, absent: Set<string>) => [...source].filter(item => !absent.has(item)).sort();
|
|
53
|
+
return { carried: [...baseSet].filter(item => candidateSet.has(item)).sort(), new: list(candidateSet, baseSet), resolved: list(baseSet, candidateSet) };
|
|
54
|
+
}
|