@afokapu/atdd-bun 0.10.52 → 0.10.54
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 +10 -0
- package/integrity.json +5 -5
- package/package.json +1 -1
- package/src/cli.ts +9 -4
- package/src/hooks.ts +4 -3
- package/src/ratchet.ts +11 -0
package/README.md
CHANGED
|
@@ -143,6 +143,16 @@ The command runs the same installed detectors against an isolated exact base and
|
|
|
143
143
|
|
|
144
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
145
|
|
|
146
|
+
### One-time explicit profile activation
|
|
147
|
+
|
|
148
|
+
When an existing exact main has debt under a smaller explicit profile list, a separately governed adoption commit may expand that list without accepting the debt. The base and candidate are both judged with the candidate's newly selected profile set, so old findings are `carried`, candidate-only findings remain `new` and reject `reject-new`, and removals are reported as `resolved`:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
bun run atdd-bun --profile flow,traceability --ratchet --ratchet-activate-profiles --base <40-character-reviewed-main-sha>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
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.
|
|
155
|
+
|
|
146
156
|
## Docs-site theme
|
|
147
157
|
|
|
148
158
|
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.54",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "e3d4858a387637e74294f5e249ab2c377a57912363136c3e19dc3c1c3f3b3c4c",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/_profiles/architecture.yaml": "f1808768b720a4e476047e4a0912002ca673946562cea9aeef5d0c214828d93a",
|
|
9
9
|
"conventions/_profiles/coder.yaml": "3fb191556a64c9ebcf249516afa189afca24da9b22fd40b2fc66908196cdfb78",
|
|
@@ -1195,10 +1195,10 @@
|
|
|
1195
1195
|
"relationships.yaml": "5860a308ebc3ee7fa8172fc2c6633984000ab3003f0ae4b6fa28db48879eda22",
|
|
1196
1196
|
"src/agent.ts": "6d6c6e75908af8fbab13432f0001b0374bb25cf3dee46ba218dfd67e0090cb56",
|
|
1197
1197
|
"src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
|
|
1198
|
-
"src/cli.ts": "
|
|
1198
|
+
"src/cli.ts": "c4ede85983445a6388bf74bbccf1251dd45df2788a699f1bf2250fa0fda3c516",
|
|
1199
1199
|
"src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
|
|
1200
1200
|
"src/enforce.ts": "7e6b3a1fc7c90f8e1836ab3e4655778fb569a63be5e7c199de44024fc003e6be",
|
|
1201
|
-
"src/hooks.ts": "
|
|
1201
|
+
"src/hooks.ts": "233d26c0320adea3b315d808811da9b53b989db8effd4f6ed9306a80069101e8",
|
|
1202
1202
|
"src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
|
|
1203
1203
|
"src/integrity.ts": "f5ad22759eec675454df82cdcf791b1f081951f08ca5084ebdcb8cd82aa1ecef",
|
|
1204
1204
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
@@ -1206,7 +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": "
|
|
1209
|
+
"src/ratchet.ts": "1e90fba040fd90437cd9d0300bcd2dc575a3952a7e5919cc2d5c8ff59814f1f1",
|
|
1210
1210
|
"src/register.ts": "6a2f1c6b8df0dc9c8f45df8ec9fac2be29044e048b08c8ffe423410e94dadedd",
|
|
1211
1211
|
"src/release.ts": "7b0fe444ee81f476f53b9c21b6c347ef588ee616c60211419cf984132be768ba",
|
|
1212
1212
|
"src/setup.ts": "38233265afedc63499bbef3b5403889651add335a70efd3813254ba53f4fc4fe",
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { enforce, enabledProfiles, flowCompatibilityDiagnostic, normalizeProfileName, profileNames, type Profile } from "./enforce";
|
|
3
|
-
import { compareFindings, contextDigest, parseRatchetPolicy } from "./ratchet";
|
|
3
|
+
import { activationContextDigest, compareFindings, contextDigest, isStrictProfileExpansion, parseRatchetPolicy } from "./ratchet";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -19,7 +19,7 @@ const args = process.argv.slice(2);
|
|
|
19
19
|
const usage = {
|
|
20
20
|
command: "atdd-bun",
|
|
21
21
|
usage: [
|
|
22
|
-
"atdd-bun [profile ...] [--root <path>] [--ratchet --base <full-sha>]",
|
|
22
|
+
"atdd-bun [profile ...] [--root <path>] [--ratchet --base <full-sha> [--ratchet-activate-profiles]]",
|
|
23
23
|
"atdd-bun init [--replace]",
|
|
24
24
|
"atdd-bun hooks <install|uninstall|status> [--replace]",
|
|
25
25
|
"atdd-bun worktree <start|finish|status>",
|
|
@@ -140,6 +140,8 @@ const git = async (directory: string, gitArgs: string[]) => {
|
|
|
140
140
|
return { code: await child.exited, out: (await new Response(child.stdout).text()).trim(), err: (await new Response(child.stderr).text()).trim() };
|
|
141
141
|
};
|
|
142
142
|
const stable = (items: string[]) => [...new Set(items)].sort();
|
|
143
|
+
const profileActivation = args.includes("--ratchet-activate-profiles");
|
|
144
|
+
if (profileActivation && !args.includes("--ratchet")) fail("--ratchet-activate-profiles requires --ratchet");
|
|
143
145
|
if (!args.includes("--ratchet")) {
|
|
144
146
|
const violations = await enforce({ root, profiles: normalizedRequested as Profile[] });
|
|
145
147
|
printViolations(violations);
|
|
@@ -152,6 +154,7 @@ if (!args.includes("--ratchet")) {
|
|
|
152
154
|
if (!policy) fail("--ratchet requires an explicit atdd-bun.yaml ratchet policy");
|
|
153
155
|
const candidateProfiles = normalizedRequested.includes("all") ? await enabledProfiles(candidateRoot) : normalizedRequested;
|
|
154
156
|
if (stable(policy.profiles).join(",") !== stable(candidateProfiles).join(",")) fail("ratchet.profiles must exactly equal the selected profiles");
|
|
157
|
+
if (profileActivation && (!Array.isArray(candidateConfig.profiles) || stable(await enabledProfiles(candidateRoot)).join(",") !== stable(candidateProfiles).join(","))) fail("profile activation requires the explicitly configured candidate profile set to be selected");
|
|
155
158
|
if ((await git(candidateRoot, ["status", "--porcelain"])).out) fail("--ratchet requires a porcelain-clean candidate worktree");
|
|
156
159
|
const resolved = await git(candidateRoot, ["rev-parse", "--verify", `${base}^{commit}`]);
|
|
157
160
|
if (resolved.code || resolved.out.toLowerCase() !== base.toLowerCase()) fail(`ratchet base ${base} is unavailable or not a full exact commit`);
|
|
@@ -164,8 +167,10 @@ if (!args.includes("--ratchet")) {
|
|
|
164
167
|
const checkedOut = await git(scratch, ["checkout", "--detach", base]);
|
|
165
168
|
if (checkedOut.code) { console.error(`could not checkout ratchet base: ${checkedOut.err || checkedOut.out}`); process.exitCode = 1; }
|
|
166
169
|
else {
|
|
167
|
-
const baseConfig = await configAt(scratch), baseProfiles = normalizedRequested.includes("all") ? await enabledProfiles(scratch) : normalizedRequested;
|
|
168
|
-
|
|
170
|
+
const baseConfig = await configAt(scratch), baseProfiles = profileActivation ? await enabledProfiles(scratch) : normalizedRequested.includes("all") ? await enabledProfiles(scratch) : normalizedRequested;
|
|
171
|
+
const ordinaryContextMatches = stable(baseProfiles).join(",") === stable(candidateProfiles).join(",") && contextDigest(baseConfig, baseProfiles) === contextDigest(candidateConfig, candidateProfiles);
|
|
172
|
+
const activationIsValid = Array.isArray(baseConfig.profiles) && isStrictProfileExpansion(baseProfiles, candidateProfiles) && activationContextDigest(baseConfig) === activationContextDigest(candidateConfig);
|
|
173
|
+
if (!(profileActivation ? activationIsValid : ordinaryContextMatches)) { console.error(profileActivation ? "ratchet profile activation requires an explicit monotonic profile expansion and identical non-profile context" : "ratchet base and candidate profile/context differ; use a separately governed adoption path"); process.exitCode = 1; }
|
|
169
174
|
else {
|
|
170
175
|
const [baseFindings, candidateFindings] = await Promise.all([enforce({ root: scratch, profiles: candidateProfiles as Profile[] }), enforce({ root: candidateRoot, profiles: candidateProfiles as Profile[] })]);
|
|
171
176
|
const normalize = (findings: Awaited<ReturnType<typeof enforce>>, directory: string) => findings.map(finding => ({ ...finding, file: relative(directory, finding.file).replaceAll("\\", "/") }));
|
package/src/hooks.ts
CHANGED
|
@@ -47,8 +47,9 @@ export async function runHook(event: HookEvent, root = process.cwd(), args: stri
|
|
|
47
47
|
return { ok: true, message: "ok" };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Resolve from the checkout at hook time: installed consumers use their local package; source checkouts use src.
|
|
51
|
+
const dispatcher = (event: HookEvent) => `#!/bin/sh\nroot="$(git rev-parse --show-toplevel)" || exit $?\nentry="$root/node_modules/@afokapu/atdd-bun/src/cli.ts"\nif [ ! -f "$entry" ]; then\n if [ -f "$root/package.json" ] && grep -Eq '"name"[[:space:]]*:[[:space:]]*"@afokapu/atdd-bun"' "$root/package.json" && [ -f "$root/src/cli.ts" ]; then\n entry="$root/src/cli.ts"\n else\n echo "atdd-bun hook: install @afokapu/atdd-bun in this project before running hooks" >&2\n exit 1\n fi\nfi\nexec bun "$entry" hook ${event} "$@"\n`;
|
|
52
|
+
export async function installHooks(root = process.cwd(), replace = false) { const current = (await git(root, ["config", "--worktree", "--get", "core.hooksPath"])).out || (await git(root, ["config", "--get", "core.hooksPath"])).out; if (current && current !== ".githooks" && !replace) return bad(`core.hooksPath is ${current}; use --replace`); await git(root, ["config", "extensions.worktreeConfig", "true"]); await mkdir(join(root, ".githooks"), { recursive: true }); for (const event of hookEvents) { const file = join(root, ".githooks", event); await writeFile(file, dispatcher(event)); await chmod(file, 0o755); } const result = await git(root, ["config", "--worktree", "core.hooksPath", ".githooks"]); return result.code ? bad(result.err) : { ok: true, message: "hooks installed" }; }
|
|
52
53
|
export async function uninstallHooks(root = process.cwd()) { if ((await git(root, ["config", "--worktree", "--get", "core.hooksPath"])).out === ".githooks") await git(root, ["config", "--worktree", "--unset", "core.hooksPath"]); for (const event of hookEvents) await rm(join(root, ".githooks", event), { force: true }); return { ok: true, message: "hooks uninstalled" }; }
|
|
53
54
|
export async function hooksStatus(root = process.cwd()) { const path = (await git(root, ["config", "--worktree", "--get", "core.hooksPath"])).out; return { ok: path === ".githooks" && hookEvents.every(event => existsSync(join(root, ".githooks", event))), message: path || "not installed" }; }
|
|
54
55
|
|
|
@@ -57,6 +58,6 @@ export async function startWorktree(root = process.cwd(), branch = "") { const c
|
|
|
57
58
|
|
|
58
59
|
export async function worktreeStatus(root = process.cwd()) { const cfg = await policy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); const result = await git(state.primary, ["worktree", "list", "--porcelain"]); return result.code ? bad(result.err) : { ok: true, message: result.out }; }
|
|
59
60
|
|
|
60
|
-
async function generatedHooksOnly(root: string): Promise<boolean> { const directory = join(root, ".githooks"); if (!existsSync(directory)) return false; const names = (await readdir(directory)).sort(); if (names.join("\n") !== [...hookEvents].sort().join("\n")) return false;
|
|
61
|
+
async function generatedHooksOnly(root: string): Promise<boolean> { const directory = join(root, ".githooks"); if (!existsSync(directory)) return false; const names = (await readdir(directory)).sort(); if (names.join("\n") !== [...hookEvents].sort().join("\n")) return false; return (await Promise.all(hookEvents.map(async event => (await readFile(join(directory, event), "utf8")) === dispatcher(event)))).every(Boolean); }
|
|
61
62
|
|
|
62
63
|
export async function finishWorktree(root = process.cwd(), deleteBranch = false) { const cfg = await policy(root); if (!cfg.worktrees.enabled) return bad("worktree policy is disabled"); const state = await layout(root, cfg.worktrees); if ("error" in state) return bad(state.error); if (state.current === state.primary || !inside(state.worktreeRoot, state.current)) return bad(`finish must run from a linked worktree beneath ${state.worktreeRoot}`); if (!state.branch || cfg.protected_branches.includes(state.branch)) return bad("finish requires a non-protected named branch"); const status = (await git(state.current, ["status", "--porcelain"])).out, generatedHooks = await generatedHooksOnly(state.current), onlyGeneratedHooks = Boolean(status) && status.split("\n").every(line => line.endsWith(" .githooks/") || line.endsWith(" .githooks")); if (status && !onlyGeneratedHooks) return bad("worktree is dirty; commit, stash, or discard changes before finish"); if ((await git(state.primary, ["merge-base", "--is-ancestor", state.branch, cfg.worktrees.primary_branch])).code) return bad(`branch ${state.branch} is not merged into ${cfg.worktrees.primary_branch} in the primary checkout`); const removed = await git(state.primary, ["worktree", "remove", ...(onlyGeneratedHooks && generatedHooks ? ["--force"] : []), state.current]); if (removed.code) return bad(removed.err || removed.out); if (deleteBranch) { const deleted = await git(state.primary, ["branch", "-d", state.branch]); if (deleted.code) return bad(`worktree removed, but branch deletion failed: ${deleted.err || deleted.out}`); } return { ok: true, message: `removed merged worktree ${state.current}${deleteBranch ? ` and branch ${state.branch}` : ""}` }; }
|
package/src/ratchet.ts
CHANGED
|
@@ -37,6 +37,17 @@ export function contextDigest(config: Record<string, unknown>, profiles: string[
|
|
|
37
37
|
return sha256({ format: "atdd-bun.ratchet-context/v1", profiles: [...profiles].sort(), policy });
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Activation is a one-time, explicitly requested profile-list expansion; every other policy input remains identical. */
|
|
41
|
+
export function activationContextDigest(config: Record<string, unknown>): string {
|
|
42
|
+
const { ratchet: _ratchet, profiles: _profiles, ...policy } = config;
|
|
43
|
+
return sha256({ format: "atdd-bun.ratchet-activation-context/v1", policy });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isStrictProfileExpansion(base: string[], candidate: string[]): boolean {
|
|
47
|
+
const before = new Set(base), after = new Set(candidate);
|
|
48
|
+
return before.size < after.size && [...before].every(profile => after.has(profile));
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export function parseRatchetPolicy(config: Record<string, unknown>): RatchetPolicy | null {
|
|
41
52
|
if (config.ratchet === undefined) return null;
|
|
42
53
|
const value = config.ratchet;
|