@afokapu/atdd-bun 0.10.78 → 0.10.80
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.
|
@@ -56,6 +56,7 @@ content:
|
|
|
56
56
|
- Test, spec, and `.d.ts` files are excluded, as are `index.ts` barrels and `__tests__` directories.
|
|
57
57
|
- Trivial lines (bare braces, closing punctuation) are not counted toward the 7.
|
|
58
58
|
- Import and re-export declarations are not counted, because two files importing the same names from one shared module match because the logic was shared, which is the fix this rule asks for.
|
|
59
|
+
- Property signatures inside a `type X = {` or `interface X {` body (optional `readonly`, a name, optional `?`, `:` a type, ending `;` or `,`), including the members of nested object types, are not counted, because two unrelated records with the same number of same-typed fields match once identifiers are folded and neither coupling them nor a cosmetic edit is a fix. Executable statements, object literals, class fields, enum bodies, and method bodies still count.
|
|
59
60
|
- Files whose layer cannot be determined are compared only against each other.
|
|
60
61
|
metadata:
|
|
61
62
|
aliases:
|
|
@@ -234,19 +234,50 @@ export const isTrivial = (n) => TRIVIAL.has(n);
|
|
|
234
234
|
const DECLARATION = /^(import\b|export (type )?(\{|\*))/;
|
|
235
235
|
const DECLARATION_END = /\bfrom "S"|^import "S"|\}/;
|
|
236
236
|
|
|
237
|
+
// Property signatures inside a `type X = {` / `interface X {` body are not code either: once
|
|
238
|
+
// identifiers are folded, two unrelated records with seven `readonly id: string;` members match
|
|
239
|
+
// line for line, and neither coupling the records nor a cosmetic edit is a fix. A member line
|
|
240
|
+
// (optional `readonly`, a name, optional `?`, `:` a type, ending `;` or `,`), a member that opens a
|
|
241
|
+
// nested object type, and that nested type's closing line are left out of the windows. The body
|
|
242
|
+
// is tracked by brace depth; anything else inside it (a method signature, say) still counts.
|
|
243
|
+
const TYPE_BODY = /^(export )?(declare )?(type ID(<[^>]*>)? = \{|interface ID(<.*>)?( extends [^{]*)? \{)$/;
|
|
244
|
+
const MEMBER = /^(readonly )?(ID|"S")\??: (.*[;,]|\{)$|^\}[;,]?$/;
|
|
245
|
+
const braceDelta = (n) => (n.match(/\{/g) ?? []).length - (n.match(/\}/g) ?? []).length;
|
|
246
|
+
// normalizeLine strips a block comment only when it closes on the same line, so a multi-line
|
|
247
|
+
// comment's text (say a JSDoc ` * shape like { a`) would reach braceDelta and keep the body open
|
|
248
|
+
// for the rest of the file. The body is tracked on the code part of each line only; whether the
|
|
249
|
+
// line counts toward a window is unchanged.
|
|
250
|
+
function codePart(n, inComment) {
|
|
251
|
+
let code = n;
|
|
252
|
+
if (inComment) {
|
|
253
|
+
const close = code.indexOf("*/");
|
|
254
|
+
if (close < 0) return { code: "", inComment: true };
|
|
255
|
+
code = code.slice(close + 2).trim();
|
|
256
|
+
}
|
|
257
|
+
const open = code.indexOf("/*");
|
|
258
|
+
return open < 0 ? { code, inComment: false } : { code: code.slice(0, open).trim(), inComment: true };
|
|
259
|
+
}
|
|
260
|
+
|
|
237
261
|
// A 16-hex-char digest of each `minLines` window of non-trivial normalized lines —
|
|
238
262
|
// the same fingerprint shape the Python detector hashes with sha256[:16].
|
|
239
263
|
export function fragments(source, minLines, hasher) {
|
|
240
264
|
const nonTrivial = [];
|
|
241
265
|
let inDeclaration = false;
|
|
266
|
+
let typeDepth = 0;
|
|
267
|
+
let inComment = false;
|
|
242
268
|
source.split("\n").forEach((line, i) => {
|
|
243
269
|
const n = normalizeLine(line);
|
|
270
|
+
const part = codePart(n, inComment);
|
|
271
|
+
inComment = part.inComment;
|
|
244
272
|
if (!inDeclaration && DECLARATION.test(n)) inDeclaration = true;
|
|
245
273
|
if (inDeclaration) {
|
|
246
274
|
if (DECLARATION_END.test(n)) inDeclaration = false;
|
|
247
275
|
return;
|
|
248
276
|
}
|
|
249
|
-
|
|
277
|
+
const member = typeDepth > 0 && MEMBER.test(part.code);
|
|
278
|
+
if (typeDepth > 0) typeDepth = Math.max(0, typeDepth + braceDelta(part.code));
|
|
279
|
+
else if (TYPE_BODY.test(part.code)) typeDepth = 1;
|
|
280
|
+
if (!member && !isTrivial(n)) nonTrivial.push([i + 1, n]);
|
|
250
281
|
});
|
|
251
282
|
if (nonTrivial.length < minLines) return [];
|
|
252
283
|
const out = [];
|
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.80",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"conventions/coder.bun/coder.bun.dto-mapper.convention.yaml": "c524b5f37db6ae4f7558fc3ec672aa1c5b764738a0f25a20062d736fed6bbcb3",
|
|
60
60
|
"conventions/coder.bun/coder.bun.dto-placement.convention.yaml": "9525255f9e037ca0d939de20e70235fe348dcebf2136536055bc21433d7e9fb5",
|
|
61
61
|
"conventions/coder.bun/coder.bun.dto-purity.convention.yaml": "4743199fb242a25f896425916e3873c2e8cf62f4285437f62b0f8f8139d6a58d",
|
|
62
|
-
"conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "
|
|
62
|
+
"conventions/coder.bun/coder.bun.duplication-intra-layer.convention.yaml": "023310241a28e6a7aad5cf020bd1b9d4e05a887ddd92dbcd4c13b8a45e26ae6e",
|
|
63
63
|
"conventions/coder.bun/coder.bun.error-response-bare-string.convention.yaml": "c2c0f859f6dd12702edf225c4148849e72b6c25e6d7cd28edd5d644ba277a23c",
|
|
64
64
|
"conventions/coder.bun/coder.bun.error-response-code-format.convention.yaml": "3048fed00b92c2559f7c52f73cfcce07b583eb47b12d04f0851e237ae3646567",
|
|
65
65
|
"conventions/coder.bun/coder.bun.green-header-order.convention.yaml": "2a42d67bedeec1de76fa1de844351983c5ba4bc5b61d5a5047298f6804664f67",
|
|
@@ -741,7 +741,7 @@
|
|
|
741
741
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/long.ts": "b87149b1b550a00bc2de2b2b21a4d9925572c002221941fae8135e8fe0c27cd1",
|
|
742
742
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/orphan.ts": "0f7acd0dffe90902a0a43cc45771b8a473d079efc9693112b3b85645c191c23d",
|
|
743
743
|
"detectors/bun_ts_metrics_detector/fixtures/dirty/typed.ts": "7c27ee1a674af7d5b2cce3a65bfec2111602d84a75817fd60f737c7d63fc9c83",
|
|
744
|
-
"detectors/bun_ts_metrics_detector/ts_metrics.mjs": "
|
|
744
|
+
"detectors/bun_ts_metrics_detector/ts_metrics.mjs": "7ccfd393b38b0d0f45ab13bffaefdeeef782818af5194d8ff3d6fce5ae34ff44",
|
|
745
745
|
"detectors/htmx_e2e_detector/atdd.implementation.yaml": "454bb123039d57c992d27d9274e20fa3452ac41a237eb55471be62a4bcfebf06",
|
|
746
746
|
"detectors/htmx_e2e_detector/checks/_e2e.mjs": "efec6c8e3dc8b95bf3acd826397ecdedac64c06d367791a4017a272bc0660896",
|
|
747
747
|
"detectors/htmx_e2e_detector/checks/_map.json": "94dec08ff4764cd6a3dba139f12a7c984292e95a9354b0cdaa0ce9d4180e2c89",
|
|
@@ -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": "5c7dd77c370e745ad7a6f308fa0cfaeeb066560540403d2a4f56543a570116cc",
|
|
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
|
@@ -131,6 +131,19 @@ async function protectedRemoteTips(root: string, protectedBranches: string[], re
|
|
|
131
131
|
return { tips: [...new Set(tips)] };
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/** Commits already on the push remote do not count: protected tips plus the SHAs the remote itself advertises for refs/heads/* (one ls-remote per push) whose commit exists locally. Local tracking SHAs, stale refs and local branches never exempt; an unreadable remote falls back to protected tips only. */
|
|
135
|
+
async function countExclusionTips(root: string, protectedBranches: string[], remote: string | undefined): Promise<{ tips: string[] } | { error: string }> {
|
|
136
|
+
const protectedTips = await protectedRemoteTips(root, protectedBranches, remote);
|
|
137
|
+
if ("error" in protectedTips) return protectedTips;
|
|
138
|
+
const remotes = (await git(root, ["remote"])).out.split("\n").filter(Boolean);
|
|
139
|
+
if (!remote || !remotes.includes(remote)) return protectedTips;
|
|
140
|
+
const advertised = await git(root, ["ls-remote", "--heads", remote]);
|
|
141
|
+
if (advertised.code) return protectedTips;
|
|
142
|
+
const shas = [...new Set(advertised.out.split("\n").filter(Boolean).flatMap(row => { const [sha, ref] = row.split(/\s+/); return ref?.startsWith("refs/heads/") && /^[0-9a-f]{40}$/i.test(sha ?? "") ? [sha!.toLowerCase()] : []; }))];
|
|
143
|
+
const present = (await Promise.all(shas.map(async sha => (await git(root, ["cat-file", "-e", `${sha}^{commit}`])).code ? [] : [sha]))).flat();
|
|
144
|
+
return { tips: [...new Set([...protectedTips.tips, ...present])] };
|
|
145
|
+
}
|
|
146
|
+
|
|
134
147
|
/** A brand-new remote branch is ratcheted against the one protected remote-tracking tip that is an exact ancestor of the candidate. */
|
|
135
148
|
async function protectedAncestors(root: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ ancestors: string[] } | { error: string }> {
|
|
136
149
|
const found = await protectedRemoteTips(root, protectedBranches, remote);
|
|
@@ -230,7 +243,7 @@ export async function runHook(event: HookEvent, root = process.cwd(), args: stri
|
|
|
230
243
|
if (["pre-commit", "pre-merge-commit"].includes(event)) { const violation = await worktreeCommitPolicy(root, cfg.worktrees); if (violation) return bad(violation); }
|
|
231
244
|
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" }; }
|
|
232
245
|
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" }; }
|
|
233
|
-
if (event === "pre-push") { const reports: string[] = []; for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [, 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 isNew = !remoteSha || /^0+$/.test(remoteSha), range = isNew ? `${local}^..${local}` : `${remoteSha}..${local}`, shared = await
|
|
246
|
+
if (event === "pre-push") { const reports: string[] = []; let exclusions: Awaited<ReturnType<typeof countExclusionTips>> | undefined; for (const row of stdin.split("\n").filter(Boolean).map(row => row.split(/\s+/))) { const [, 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 isNew = !remoteSha || /^0+$/.test(remoteSha), range = isNew ? `${local}^..${local}` : `${remoteSha}..${local}`, shared = exclusions ??= await countExclusionTips(root, cfg.protected_branches, args[0]); if ("error" in shared) return bad(`could not list remote branch tips for the pushed range ${range}: ${shared.error}`); const counted = await git(root, ["rev-list", "--count", ...(shared.tips.length ? [isNew ? local : range, "--not", ...shared.tips] : [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 = remote?.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" }; }
|
|
234
247
|
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}` }; }
|
|
235
248
|
return { ok: true, message: "ok" };
|
|
236
249
|
}
|