@afokapu/atdd-bun 0.10.83 → 0.10.84
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 +2 -2
- package/package.json +1 -1
- package/src/hooks.ts +60 -12
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.84",
|
|
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": "
|
|
1264
|
+
"src/hooks.ts": "f20024340979de799ed977b8d2483ca09c494ef952dcea2cc7c4c39e6e10679c",
|
|
1265
1265
|
"src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
|
|
1266
1266
|
"src/integrity.ts": "f5ad22759eec675454df82cdcf791b1f081951f08ca5084ebdcb8cd82aa1ecef",
|
|
1267
1267
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { chmod, mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { concreteProfiles, enabledProfiles, enforce, normalizeProfileName } from "./enforce";
|
|
6
|
-
import { activationContextDigest, canonicalFindingPath, compareFindings, contextDigest, isStrictProfileExpansion, parseRatchetPolicy } from "./ratchet";
|
|
5
|
+
import { concreteProfiles, enabledProfiles, enforce, normalizeProfileName, profileConventions } from "./enforce";
|
|
6
|
+
import { activationContextDigest, canonicalFindingPath, compareFindings, contextDigest, findingFingerprint, isStrictProfileExpansion, parseRatchetPolicy, type FindingIdentity } from "./ratchet";
|
|
7
7
|
import { topologyFor } from "./topology";
|
|
8
8
|
import { JOURNEY_DOCS_DIR } from "./journey-docs";
|
|
9
9
|
|
|
@@ -113,8 +113,9 @@ async function strictPushGate(root: string, candidate: string, range: string) {
|
|
|
113
113
|
const candidateRoot = await mkdtemp(join(tmpdir(), "atdd-strict-push-candidate-"));
|
|
114
114
|
try {
|
|
115
115
|
await checkoutCommit(root, candidateRoot, candidate, "strict pre-push candidate");
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
// The same affected-area selection as pre-commit, judged against the candidate's own registry policy.
|
|
117
|
+
const changed = await files(candidateRoot, ["diff", "--name-only", range]), cfg = await policy(candidateRoot);
|
|
118
|
+
return await validation(candidateRoot, changed, false, changed.some(path => isRegistry(cfg, path)));
|
|
118
119
|
} catch (error) { return bad(`strict pre-push gate failed closed: ${String(error)}`); }
|
|
119
120
|
finally { await git(root, ["worktree", "remove", "--force", candidateRoot]); await rm(candidateRoot, { recursive: true, force: true }); }
|
|
120
121
|
}
|
|
@@ -170,6 +171,49 @@ async function existingBranchProtectedBase(root: string, candidate: string, remo
|
|
|
170
171
|
return { base: tip && tip !== remoteBase.toLowerCase() ? tip : null };
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/** Ratchet findings depend only on the scanned tree, the toolkit scanning it and the selected profiles, so they are cached
|
|
175
|
+
* under the Git common directory. An entry is advisory: unreadable, malformed or differently keyed means recompute, never clean. */
|
|
176
|
+
const packageRoot = resolve(import.meta.dir, ".."), toolkitInputs = ["package.json", "relationships.yaml", "src", "lib", "detectors", "conventions", "planner-nodes", "planner-schemas"];
|
|
177
|
+
export type ToolkitIdentity = { version: string; digest: string };
|
|
178
|
+
async function toolkitFiles() {
|
|
179
|
+
const found = new Set<string>();
|
|
180
|
+
for (const input of toolkitInputs) {
|
|
181
|
+
const path = join(packageRoot, input);
|
|
182
|
+
if (!existsSync(path)) continue;
|
|
183
|
+
if ((await stat(path)).isFile()) { found.add(input); continue; }
|
|
184
|
+
for await (const file of new Bun.Glob("**/*").scan({ cwd: path, onlyFiles: true, dot: true })) found.add(`${input}/${file.replaceAll("\\", "/")}`);
|
|
185
|
+
}
|
|
186
|
+
// Companion conventions (the Flow lifecycle) live outside this package but still shape findings.
|
|
187
|
+
for (const { path } of Object.values(profileConventions).flat()) found.add(path);
|
|
188
|
+
return [...found].sort();
|
|
189
|
+
}
|
|
190
|
+
async function computeToolkitIdentity(): Promise<ToolkitIdentity> {
|
|
191
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
192
|
+
for (const file of await toolkitFiles()) { const full = resolve(packageRoot, file); hasher.update(`${file}\0`).update(existsSync(full) ? await readFile(full) : "\0missing").update("\0"); }
|
|
193
|
+
const version = (JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")) as { version?: unknown }).version;
|
|
194
|
+
return { version: typeof version === "string" ? version : "unknown", digest: hasher.digest("hex") };
|
|
195
|
+
}
|
|
196
|
+
let identity: Promise<ToolkitIdentity> | undefined;
|
|
197
|
+
/** The running toolkit's package version and a digest of every detector/convention input, computed once per process. */
|
|
198
|
+
export function toolkitIdentity(): Promise<ToolkitIdentity> { return identity ??= computeToolkitIdentity(); }
|
|
199
|
+
const cachedEntry = (text: string, key: string): FindingIdentity[] | null => {
|
|
200
|
+
const entry = JSON.parse(text) as { schema?: unknown; key?: unknown; findings?: unknown };
|
|
201
|
+
if (entry?.schema !== "atdd-bun.findings-cache/v1" || JSON.stringify(entry.key) !== key || !Array.isArray(entry.findings)) return null;
|
|
202
|
+
const valid = entry.findings.every(finding => finding && typeof finding.rule_id === "string" && typeof finding.file === "string" && findingFingerprint(finding));
|
|
203
|
+
return valid ? entry.findings : null;
|
|
204
|
+
};
|
|
205
|
+
/** Findings for one exact tree: reuse a valid entry, otherwise compute and record the result for the next push. */
|
|
206
|
+
async function cachedFindings(directory: string | null, tree: string, toolkit: ToolkitIdentity | null, profiles: string[], compute: () => Promise<FindingIdentity[]>): Promise<FindingIdentity[]> {
|
|
207
|
+
if (!directory || !toolkit || !/^[0-9a-f]{40}$/i.test(tree)) return compute();
|
|
208
|
+
const key = JSON.stringify({ schema: "atdd-bun.findings-cache-key/v1", tree: tree.toLowerCase(), version: toolkit.version, digest: toolkit.digest, profiles: stable(profiles) });
|
|
209
|
+
const file = join(directory, `${new Bun.CryptoHasher("sha256").update(key).digest("hex")}.json`);
|
|
210
|
+
const hit = await readFile(file, "utf8").then(text => cachedEntry(text, key)).catch(() => null);
|
|
211
|
+
if (hit) return hit;
|
|
212
|
+
const findings = await compute(), scratch = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
213
|
+
await mkdir(directory, { recursive: true }).then(() => writeFile(scratch, JSON.stringify({ schema: "atdd-bun.findings-cache/v1", key: JSON.parse(key), findings }))).then(() => rename(scratch, file)).catch(() => rm(scratch, { force: true }));
|
|
214
|
+
return findings;
|
|
215
|
+
}
|
|
216
|
+
|
|
173
217
|
async function ratchetPushGate(root: string, remoteBase: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ ok: boolean; message: string } | null> {
|
|
174
218
|
let base = remoteBase;
|
|
175
219
|
if (!/^[0-9a-f]{40}$/i.test(candidate)) return bad("ratchet reject-new requires an exact local candidate commit");
|
|
@@ -213,13 +257,17 @@ async function ratchetPushGate(root: string, remoteBase: string, candidate: stri
|
|
|
213
257
|
// Only a protected tip judged under the same profile/context as the candidate is comparable; otherwise the remote tip alone decides.
|
|
214
258
|
if (contextDigest(protectedConfig, candidateProfiles) === contextDigest(candidateConfig, candidateProfiles)) protectedUsed = protectedBase;
|
|
215
259
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
// Findings of an exact tree are reused across pushes; uncached enforcements run one at a time to cap peak processes.
|
|
261
|
+
const common = await git(root, ["rev-parse", "--git-common-dir"]), cacheDirectory = common.code || !common.out ? null : join(resolve(root, common.out), "atdd-bun", "findings-cache");
|
|
262
|
+
const toolkit = await toolkitIdentity().catch(() => null);
|
|
263
|
+
const findingsAt = async (directory: string, commit: string) => {
|
|
264
|
+
const tree = await git(root, ["rev-parse", "--verify", `${commit}^{tree}`]);
|
|
265
|
+
return cachedFindings(cacheDirectory, tree.code ? "" : tree.out, toolkit, candidateProfiles, async () => normalize(await enforce({ root: directory, profiles: candidateProfiles as any }), directory));
|
|
266
|
+
};
|
|
267
|
+
const baseFindings = await findingsAt(baseRoot, base);
|
|
268
|
+
const protectedFindings = protectedUsed ? await findingsAt(protectedRoot, protectedUsed) : [];
|
|
269
|
+
const candidateFindings = await findingsAt(candidateRoot, candidate);
|
|
270
|
+
const delta = compareFindings([...baseFindings, ...protectedFindings], candidateFindings);
|
|
223
271
|
return { ok: delta.new.length === 0, message: JSON.stringify({ schema: "atdd-bun.ratchet-report/v1", mode: policy.mode, base, ...(protectedUsed ? { protected_base: protectedUsed } : {}), candidate, profiles: candidateProfiles, context: contextDigest(candidateConfig, candidateProfiles), ...delta }) };
|
|
224
272
|
} catch (error) { return bad(`ratchet pre-push gate failed closed: ${String(error)}`); }
|
|
225
273
|
finally {
|