@afokapu/atdd-bun 0.10.69 → 0.10.70

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/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.69",
2
+ "version": "0.10.70",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -1261,7 +1261,7 @@
1261
1261
  "src/cli.ts": "995e41b3cb44c04f65f4d599f0fc51b93b17b7558b7c7dcfabed64de3d455250",
1262
1262
  "src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
1263
1263
  "src/enforce.ts": "7e6b3a1fc7c90f8e1836ab3e4655778fb569a63be5e7c199de44024fc003e6be",
1264
- "src/hooks.ts": "6b579c030bb4772f08806e802f4f814444b1878bbac0d6c62bc10b4ee184e449",
1264
+ "src/hooks.ts": "06b71d74378c000e13339b7a9d2366e6d36993a1d9a8b84e8e8234de97d5a323",
1265
1265
  "src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
1266
1266
  "src/integrity.ts": "f5ad22759eec675454df82cdcf791b1f081951f08ca5084ebdcb8cd82aa1ecef",
1267
1267
  "src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.69",
3
+ "version": "0.10.70",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/hooks.ts CHANGED
@@ -118,7 +118,24 @@ async function strictPushGate(root: string, candidate: string, range: string) {
118
118
  finally { await git(root, ["worktree", "remove", "--force", candidateRoot]); await rm(candidateRoot, { recursive: true, force: true }); }
119
119
  }
120
120
 
121
- async function ratchetPushGate(root: string, base: string, candidate: string): Promise<{ ok: boolean; message: string } | null> {
121
+ /** A brand-new remote branch is ratcheted against the one protected remote-tracking tip that is an exact ancestor of the candidate. */
122
+ async function newBranchBase(root: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ base: string } | { error: string }> {
123
+ const listed = await git(root, ["for-each-ref", "--format=%(refname) %(objectname)", "refs/remotes/"]);
124
+ if (listed.code) return { error: `could not list protected branch tips for the new remote branch: ${listed.err || listed.out}` };
125
+ const remotes = (await git(root, ["remote"])).out.split("\n").filter(Boolean), pushRemote = remote && remotes.includes(remote) ? remote : undefined;
126
+ const tips = listed.out.split("\n").filter(Boolean).flatMap(row => {
127
+ const [ref, sha] = row.split(" "), parts = ref!.split("/");
128
+ return parts[2] && (!pushRemote || parts[2] === pushRemote) && protectedBranches.includes(parts.slice(3).join("/")) && /^[0-9a-f]{40}$/i.test(sha ?? "") ? [sha!.toLowerCase()] : [];
129
+ });
130
+ const ancestors = new Set<string>();
131
+ for (const tip of new Set(tips)) if ((await git(root, ["merge-base", "--is-ancestor", tip, candidate])).code === 0) ancestors.add(tip);
132
+ if (!ancestors.size) return { error: "ratchet new remote branch requires a protected branch tip that is an exact ancestor of the pushed candidate; none found" };
133
+ if (ancestors.size > 1) return { error: `ratchet new remote branch base is ambiguous: ${ancestors.size} protected branch tips qualify with different commits` };
134
+ return { base: [...ancestors][0]! };
135
+ }
136
+
137
+ async function ratchetPushGate(root: string, remoteBase: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ ok: boolean; message: string } | null> {
138
+ let base = remoteBase;
122
139
  if (!/^[0-9a-f]{40}$/i.test(candidate)) return bad("ratchet reject-new requires an exact local candidate commit");
123
140
  const resolvedCandidate = await git(root, ["rev-parse", "--verify", `${candidate}^{commit}`]);
124
141
  if (resolvedCandidate.code || resolvedCandidate.out.toLowerCase() !== candidate.toLowerCase()) return bad(`ratchet candidate ${candidate} is unavailable or not a full exact commit`);
@@ -130,6 +147,7 @@ async function ratchetPushGate(root: string, base: string, candidate: string): P
130
147
  try { policy = parseRatchetPolicy(candidateConfig); }
131
148
  catch (error) { return bad(`ratchet pushed candidate policy is invalid: ${String(error)}`); }
132
149
  if (!policy || policy.mode !== "reject-new") return null;
150
+ if (/^0+$/.test(remoteBase)) { const found = await newBranchBase(root, candidate, protectedBranches, remote); if ("error" in found) return bad(found.error); base = found.base; }
133
151
  if (!/^[0-9a-f]{40}$/i.test(base)) return bad("ratchet reject-new requires an exact non-zero remote commit base");
134
152
  const resolvedBase = await git(root, ["rev-parse", "--verify", `${base}^{commit}`]);
135
153
  if (resolvedBase.code || resolvedBase.out.toLowerCase() !== base.toLowerCase()) return bad(`ratchet remote base ${base} is unavailable or not a full exact commit`);
@@ -141,12 +159,14 @@ async function ratchetPushGate(root: string, base: string, candidate: string): P
141
159
  catch (error) { return bad(`ratchet remote base policy is invalid: ${String(error)}`); }
142
160
  let baseProfiles: string[], candidateProfiles: string[], policyProfiles: string[], basePolicyProfiles: string[] | null;
143
161
  try {
144
- baseProfiles = exactProfiles(baseConfig, "ratchet remote base");
162
+ baseProfiles = baseConfig.profiles === undefined ? stable(await enabledProfiles(baseRoot)) : exactProfiles(baseConfig, "ratchet remote base");
145
163
  candidateProfiles = exactProfiles(candidateConfig, "ratchet pushed candidate");
146
164
  policyProfiles = exactProfiles({ profiles: policy.profiles }, "ratchet policy");
147
165
  basePolicyProfiles = basePolicy ? exactProfiles({ profiles: basePolicy.profiles }, "ratchet remote base policy") : null;
148
166
  } catch (error) { return bad(`ratchet pushed range profile state is invalid: ${String(error)}`); }
149
- if (baseProfiles.join(",") !== candidateProfiles.join(",") || policyProfiles.join(",") !== candidateProfiles.join(",") || (basePolicyProfiles && basePolicyProfiles.join(",") !== baseProfiles.join(",")) || contextDigest(baseConfig, baseProfiles) !== contextDigest(candidateConfig, candidateProfiles)) return bad("ratchet remote base and pushed candidate profile/context differ");
167
+ const activation = isStrictProfileExpansion(baseProfiles, candidateProfiles) && activationContextDigest(baseConfig) === activationContextDigest(candidateConfig);
168
+ const unchanged = baseProfiles.join(",") === candidateProfiles.join(",") && (!basePolicyProfiles || basePolicyProfiles.join(",") === baseProfiles.join(",")) && contextDigest(baseConfig, baseProfiles) === contextDigest(candidateConfig, candidateProfiles);
169
+ if (policyProfiles.join(",") !== candidateProfiles.join(",") || (!activation && !unchanged)) return bad("ratchet remote base and pushed candidate profile/context differ");
150
170
  const normalize = (findings: Awaited<ReturnType<typeof enforce>>, directory: string) => findings.map(finding => ({ ...finding, file: canonicalFindingPath(directory, finding.file) }));
151
171
  const [baseFindings, candidateFindings] = await Promise.all([
152
172
  enforce({ root: baseRoot, profiles: candidateProfiles as any }),
@@ -174,7 +194,7 @@ export async function runHook(event: HookEvent, root = process.cwd(), args: stri
174
194
  if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
175
195
  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" }; }
176
196
  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" }; }
177
- if (event === "pre-push") { const reports: string[] = []; for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [localRef, local, remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); if (!local) return bad("pre-push update has no local object id"); if (/^0+$/.test(local)) { const deletion = await deletionPushGate(root, remoteSha); if (!deletion.ok) return deletion; continue; } const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, counted = await git(root, ["rev-list", "--count", range]); if (counted.code || !/^\d+$/.test(counted.out)) return bad(`could not count commits in pushed range ${range}: ${counted.err || counted.out}`); if (Number(counted.out) > cfg.max_commits_per_push) return bad(`commits per push ${counted.out} exceed ${cfg.max_commits_per_push}`); const ratchet = localRef?.startsWith("refs/heads/") ? await ratchetPushGate(root, remoteSha, local) : null; if (ratchet) { if (!ratchet.ok) return ratchet; reports.push(ratchet.message); continue; } const strict = await strictPushGate(root, local, range); if (!strict.ok) return strict; } return reports.length ? { ok: true, message: reports.length === 1 ? reports[0]! : `[${reports.join(",")}]` } : { ok: true, message: "validation clean" }; }
197
+ if (event === "pre-push") { const reports: string[] = []; for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [localRef, local, remote, remoteSha] = row, target = remote?.replace("refs/heads/", ""); if (target && cfg.protected_branches.includes(target)) return bad(`protected branch ${target} is blocked`); if (!local) return bad("pre-push update has no local object id"); if (/^0+$/.test(local)) { const deletion = await deletionPushGate(root, remoteSha); if (!deletion.ok) return deletion; continue; } const range = !remoteSha || /^0+$/.test(remoteSha) ? `${local}^..${local}` : `${remoteSha}..${local}`, counted = await git(root, ["rev-list", "--count", range]); if (counted.code || !/^\d+$/.test(counted.out)) return bad(`could not count commits in pushed range ${range}: ${counted.err || counted.out}`); if (Number(counted.out) > cfg.max_commits_per_push) return bad(`commits per push ${counted.out} exceed ${cfg.max_commits_per_push}`); const ratchet = localRef?.startsWith("refs/heads/") ? await ratchetPushGate(root, remoteSha ?? "", local, cfg.protected_branches, args[0]) : null; if (ratchet) { if (!ratchet.ok) return ratchet; reports.push(ratchet.message); continue; } const strict = await strictPushGate(root, local, range); if (!strict.ok) return strict; } return reports.length ? { ok: true, message: reports.length === 1 ? reports[0]! : `[${reports.join(",")}]` } : { ok: true, message: "validation clean" }; }
178
198
  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}` }; }
179
199
  return { ok: true, message: "ok" };
180
200
  }