@afokapu/atdd-bun 0.9.0 → 0.9.1
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 +16 -3
- package/integrity.json +5 -5
- package/package.json +1 -1
- package/src/delivery.ts +15 -4
- package/src/integrity.ts +42 -3
- package/templates/agents/delivery/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -125,9 +125,22 @@ docs/delivery/tranches/<tranche>/evidence.yaml one tranche's review record (
|
|
|
125
125
|
docs/delivery/tranches/<tranche>/*.json the retained raw reviewer reports
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
Where delivery is adopted, the docs profile leaves the records folder
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
Where delivery is adopted, the docs profile leaves the records folder's records and data files to
|
|
129
|
+
the delivery profile: they are not authored documentation, and changing them needs no docs
|
|
130
|
+
declaration. AsciiDoc there stays documentation. Where the docs profile is active, the first tranche
|
|
131
|
+
also brings `docs/index.adoc` and `docs/delivery/index.adoc`, each with `:doc-id:` and `:status:`,
|
|
132
|
+
since every docs area needs an index.
|
|
133
|
+
|
|
134
|
+
Upgrading from 0.8.0: records under `delivery/` are reported until `delivery.root: delivery` is set
|
|
135
|
+
(a reported root change, approved once) or they are moved. A data file in an old tranche that no
|
|
136
|
+
record there names is reported on local runs, not at the merge gate; removing it is a gate change a
|
|
137
|
+
human approves, or a new tranche's record can name it.
|
|
138
|
+
|
|
139
|
+
Upgrading from 0.9.0: an `atdd-bun.yaml` field with the wrong type (a quoted number, `yes`/`no`, a
|
|
140
|
+
non-string list item, `.inf`) is now reported, and on the base branch it blocks every pull request,
|
|
141
|
+
since the policy cannot be compared. Correct such fields on the base branch before upgrading.
|
|
142
|
+
|
|
143
|
+
Every key is optional; these are the defaults:
|
|
131
144
|
|
|
132
145
|
```yaml
|
|
133
146
|
delivery:
|
package/integrity.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.9.
|
|
2
|
+
"version": "0.9.1",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
6
|
-
"README.md": "
|
|
6
|
+
"README.md": "bd4749f81d83b92e0154692f3ee8e926b0c05ba3fbb41b430da03d56d72d7508",
|
|
7
7
|
"bunfig.toml": "b9fc65eca9014c5179380259d70e76385a6a80788fa9a2df5fb3eaa5554fd2fe",
|
|
8
8
|
"conventions/atdd-bun.planner/atdd-bun.planner.acceptance-identity.convention.yaml": "81c5c773d5ee15e8d99a2c237aa9846b110b2533df45ebf407d0e3ec6ed3dd03",
|
|
9
9
|
"conventions/atdd-bun.planner/atdd-bun.planner.identity-required.convention.yaml": "e96d7c1455d0c221072d82e7f2b55da1c4f9ceb17718eb6ed96affdeef37675b",
|
|
@@ -1190,12 +1190,12 @@
|
|
|
1190
1190
|
"src/agent.ts": "b2015873940ca83bb8fdc683dbb85c2d53fe6f8c3e2b6b0b71bda895f0c1eb2f",
|
|
1191
1191
|
"src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
|
|
1192
1192
|
"src/cli.ts": "bb2047cee60f48bfe3eaee4c1b3dcd3538a0e1edc170cf58e87686117bea07a7",
|
|
1193
|
-
"src/delivery.ts": "
|
|
1193
|
+
"src/delivery.ts": "8c2f964a4acc4471b6541f980c9c6f270b33cd64c95f5fe852a401060d507975",
|
|
1194
1194
|
"src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
|
|
1195
1195
|
"src/enforce.ts": "58ad0593895912bdc1effe97430b07acf8cda066052397957558cbb331c16778",
|
|
1196
1196
|
"src/hooks.ts": "adbb72f7a83f53d596fd217f01f4ce32c5a59a23ab81993fed226fb96fd1cbd1",
|
|
1197
1197
|
"src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
|
|
1198
|
-
"src/integrity.ts": "
|
|
1198
|
+
"src/integrity.ts": "ac516b1d3a73f872d0ed2b20a27ef0ac14ecc015293bd8e51954f322f8a063bb",
|
|
1199
1199
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
1200
1200
|
"src/planner-kernel.ts": "5d3f5305fb59becb2cc97d8a03aca6b104f385805d91cc0410856e289dd6fbb1",
|
|
1201
1201
|
"src/planner-schema-validator.ts": "ce529c0936171075e93c7539b3655fe895a98dd2973ebda5775958e6ebc316d9",
|
|
@@ -1208,7 +1208,7 @@
|
|
|
1208
1208
|
"templates/agents/AGENTS.block.md": "7e9687b1eff245b66da4127b336a9e20af2f5fa273e08895188c0a0dffebd77d",
|
|
1209
1209
|
"templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
|
|
1210
1210
|
"templates/agents/atdd/SKILL.md": "b621abe22851a75b30fc0e6c339a2c0a3ed6bbf78b8c17000a4ce301e05df96d",
|
|
1211
|
-
"templates/agents/delivery/SKILL.md": "
|
|
1211
|
+
"templates/agents/delivery/SKILL.md": "9d9b80394929220f6ec1521137df02976e3240601bbc92ee6513e078442856a6",
|
|
1212
1212
|
"templates/agents/delivery/review.md": "289ee5d55ba0f321e5703a6fd8d5417d55bf0d9c6dc3103ea0f79af11c159d9c",
|
|
1213
1213
|
"templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
|
|
1214
1214
|
"templates/github/atdd-bun.yml": "bb42414f72f4b9a2fb1eb69530695e193f105a984542f4ca124c898a5f1489be"
|
package/package.json
CHANGED
package/src/delivery.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
-
import { existsSync, lstatSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import { readdir, readFile } from "node:fs/promises";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import type { PlanFinding } from "./planner-kernel";
|
|
@@ -230,6 +230,13 @@ const namedReports = (data: unknown): string[] => {
|
|
|
230
230
|
return Array.isArray(reviews) ? reviews.flatMap(review => { const report = record(review)?.report; return typeof report === "string" && report ? [report] : []; }) : [];
|
|
231
231
|
};
|
|
232
232
|
const isFolder = (path: string) => { try { return lstatSync(path).isDirectory(); } catch { return false; } };
|
|
233
|
+
/** A folder, through a symlink too: the legacy and hidden-records probes look wherever records could be. */
|
|
234
|
+
const isLink = (path: string) => { try { return lstatSync(path).isSymbolicLink(); } catch { return false; } };
|
|
235
|
+
/** Whether any folder on the way to `relative` (below `root`) is a symlink. */
|
|
236
|
+
const throughLink = (root: string, relative: string) => relative.split("/").some((_, i, parts) => isLink(join(root, ...parts.slice(0, i + 1))));
|
|
237
|
+
/** Two paths that resolve to one folder (a compatibility symlink to the root): its records are the root's, not outside it. */
|
|
238
|
+
const sameFolder = (a: string, b: string) => { try { return realpathSync(a) === realpathSync(b); } catch { return false; } };
|
|
239
|
+
const reachesFolder = (path: string) => { try { return statSync(path).isDirectory(); } catch { return false; } };
|
|
233
240
|
const regularFile = (path: string) => { try { return lstatSync(path).isFile(); } catch { return false; } };
|
|
234
241
|
|
|
235
242
|
/** Two spellings of one commit: an abbreviated SHA is a prefix of the full one. */
|
|
@@ -353,12 +360,12 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
|
|
|
353
360
|
policy = { ...policy, root: DEFAULT_ROOT };
|
|
354
361
|
}
|
|
355
362
|
// 0.8.0 kept records under delivery/ by default. Records left there under any other effective root would be unseen.
|
|
356
|
-
if (policy.root !== LEGACY_ROOT &&
|
|
363
|
+
if (policy.root !== LEGACY_ROOT && reachesFolder(join(absolute, LEGACY_ROOT)) && !sameFolder(join(absolute, LEGACY_ROOT), join(absolute, policy.root))) {
|
|
357
364
|
const legacy = (await readdir(join(absolute, LEGACY_ROOT), { withFileTypes: true })).filter(entry => entry.isDirectory() && existsSync(join(absolute, LEGACY_ROOT, entry.name, "evidence.yaml")));
|
|
358
365
|
if (legacy.length) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `tranche records under delivery/ (${legacy.map(entry => entry.name).join(", ")}) are outside the root ${policy.root}; 0.8.0 kept them there by default; set delivery.root: delivery (a root change the integrity check reports for a human to approve once), or move them there, which rewrites merged records and so needs a human-supervised merge`));
|
|
359
366
|
}
|
|
360
367
|
// The other direction: records under the default root while another root is configured are outside what is judged.
|
|
361
|
-
if (policy.root !== DEFAULT_ROOT &&
|
|
368
|
+
if (policy.root !== DEFAULT_ROOT && reachesFolder(join(absolute, DEFAULT_ROOT)) && !sameFolder(join(absolute, DEFAULT_ROOT), join(absolute, policy.root))) {
|
|
362
369
|
const hidden = (await readdir(join(absolute, DEFAULT_ROOT), { withFileTypes: true })).filter(entry => entry.isDirectory() && existsSync(join(absolute, DEFAULT_ROOT, entry.name, "evidence.yaml")));
|
|
363
370
|
if (hidden.length) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `tranche records under ${DEFAULT_ROOT} (${hidden.map(entry => entry.name).join(", ")}) are outside the configured root ${policy.root}; move them there, or remove delivery.root`));
|
|
364
371
|
}
|
|
@@ -374,7 +381,11 @@ export async function validateDelivery(root = process.cwd(), options: DeliveryOp
|
|
|
374
381
|
// Per tranche: a data file is a report only when a record in its own tranche names it.
|
|
375
382
|
const reported = new Set(files.flatMap(entry => namedReports(entry.data).filter(report => report.startsWith(`${policy.root}/${entry.tranche}/`))));
|
|
376
383
|
const unnamed = (await dataFiles(absolute, policy)).filter(path => !reported.has(path));
|
|
377
|
-
|
|
384
|
+
const rootPath = join(absolute, policy.root);
|
|
385
|
+
if (existsSync(rootPath) || isLink(rootPath)) {
|
|
386
|
+
if (isLink(rootPath) || throughLink(absolute, policy.root)) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `delivery.root ${policy.root} is, or is reached through, a symlink; the delivery root must be a real folder, whose records Git tracks`));
|
|
387
|
+
else if (!isFolder(rootPath)) findings.push(finding("delivery.config-schema", "atdd-bun.yaml", `delivery.root ${policy.root} is a file, not a folder; the records cannot be read`));
|
|
388
|
+
}
|
|
378
389
|
for (const path of [...await strayFiles(absolute, policy), ...unnamed].sort()) if (!onBase || (await git(absolute, ["diff", "--quiet", onBase, "--", path])).code) findings.push(finding("delivery.evidence-schema", path, `${path} is neither a tranche's evidence.yaml nor a report a record in its tranche names (a data file: ${REPORT_EXTENSION.source.slice(3, -2).replaceAll("|", ", ")}); the records folder holds only records and their reports`));
|
|
379
390
|
for (const entry of files) {
|
|
380
391
|
if (onBase && existsSync(join(absolute, entry.file)) && !(await git(absolute, ["diff", "--quiet", onBase, "--", `${policy.root}/${entry.tranche}`])).code) continue;
|
package/src/integrity.ts
CHANGED
|
@@ -118,6 +118,36 @@ async function checkGenerated(root: string, packageRoot: string, skipWorkflow =
|
|
|
118
118
|
* its absent-means-all default is right for execution and wrong for deciding whether a policy was ever declared. */
|
|
119
119
|
const explicitProfiles = (config: { profiles?: unknown }): string[] | null => Array.isArray(config.profiles) ? config.profiles.map(String) : null;
|
|
120
120
|
|
|
121
|
+
/** A policy with null-valued hook keys (and worktrees children) removed: YAML gives null for a key with no value, and the
|
|
122
|
+
* hooks read null as absent, so the comparison must too. Other keys keep their null: delivery and profiles are read by
|
|
123
|
+
* readers that tell null apart from absent (`delivery:` with no value adopts delivery). */
|
|
124
|
+
function withoutNulls(config: Record<string, unknown>): Record<string, unknown> {
|
|
125
|
+
const out = Object.fromEntries(Object.entries(config).filter(([key, value]) => value !== null || !(key in defaultHookPolicy)));
|
|
126
|
+
const worktrees = out.worktrees;
|
|
127
|
+
if (typeof worktrees === "object" && worktrees !== null && !Array.isArray(worktrees)) out.worktrees = Object.fromEntries(Object.entries(worktrees).filter(([, value]) => value !== null));
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The hook policy fields whose value has the wrong type. The comparison below would otherwise crash on them (a string
|
|
132
|
+
* where a list is expected) or compare them as the defaults. An empty or null document is the default policy, and fine. */
|
|
133
|
+
export function policyShapeErrors(config: Record<string, unknown>): string[] {
|
|
134
|
+
const out: string[] = [];
|
|
135
|
+
for (const key of ["max_staged_files", "max_staged_changed_lines", "max_uncommitted_files", "max_commits_per_push", "max_registry_removed_lines"])
|
|
136
|
+
if (config[key] !== undefined && !(typeof config[key] === "number" && Number.isFinite(config[key]))) out.push(`${key} must be a finite number`);
|
|
137
|
+
for (const key of ["require_plan_reference", "require_traceability"]) if (config[key] !== undefined && typeof config[key] !== "boolean") out.push(`${key} must be true or false`);
|
|
138
|
+
for (const key of ["protected_branches", "registry_paths"]) if (config[key] !== undefined && !(Array.isArray(config[key]) && (config[key] as unknown[]).every(item => typeof item === "string"))) out.push(`${key} must be a list of strings`);
|
|
139
|
+
const worktrees = config.worktrees;
|
|
140
|
+
if (worktrees !== undefined) {
|
|
141
|
+
if (typeof worktrees !== "object" || worktrees === null || Array.isArray(worktrees)) out.push("worktrees must be a mapping");
|
|
142
|
+
else {
|
|
143
|
+
const layout = worktrees as Record<string, unknown>;
|
|
144
|
+
for (const key of ["enabled", "require_linked_worktree"]) if (layout[key] !== undefined && typeof layout[key] !== "boolean") out.push(`worktrees.${key} must be true or false`);
|
|
145
|
+
for (const key of ["root", "primary_directory", "primary_branch"]) if (layout[key] !== undefined && typeof layout[key] !== "string") out.push(`worktrees.${key} must be a string`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
/** Names of the policy fields in `current` that are looser than in `base`. */
|
|
122
152
|
export function loosenedPolicy(base: Partial<HookPolicy> & { profiles?: unknown; delivery?: unknown }, current: Partial<HookPolicy> & { profiles?: unknown; delivery?: unknown }): string[] {
|
|
123
153
|
const b = { ...defaultHookPolicy, ...base, worktrees: { ...defaultHookPolicy.worktrees, ...base.worktrees } }, c = { ...defaultHookPolicy, ...current, worktrees: { ...defaultHookPolicy.worktrees, ...current.worktrees } };
|
|
@@ -174,11 +204,20 @@ async function checkPolicy(root: string, base?: string, push = process.env.GITHU
|
|
|
174
204
|
const unreadable = (why: string) => [{ file: "atdd-bun.yaml", detail: `the baseline atdd-bun.yaml at ${against.slice(0, 7)} ${why}, so the policy cannot be compared`, restore: push
|
|
175
205
|
? `this push is compared with the tip it replaced (${against.slice(0, 7)}), whose atdd-bun.yaml is broken; once the repaired atdd-bun.yaml is on the branch, the next push is compared with a readable tip`
|
|
176
206
|
: `repair the atdd-bun.yaml on the base branch (git show ${against.slice(0, 7)}:atdd-bun.yaml) and land it there, which may need a maintainer; then bring that repair into this branch (rebase onto the base, or merge it in: a pull request is compared with its merge base) and re-run the check` }];
|
|
177
|
-
let baseline:
|
|
178
|
-
try { baseline = await read(before.code ? null : before.out)
|
|
207
|
+
let baseline: Record<string, unknown>;
|
|
208
|
+
try { baseline = await read(before.code ? null : before.out) as Record<string, unknown>; }
|
|
179
209
|
catch (error) { return unreadable(`could not be parsed (${String(error)})`); }
|
|
180
210
|
if (typeof baseline !== "object" || baseline === null || Array.isArray(baseline)) return unreadable(`is not a policy mapping (${JSON.stringify(baseline)})`);
|
|
181
|
-
|
|
211
|
+
// A key with no value (or only commented-out children) parses to null; the hooks read it as absent, and so does this.
|
|
212
|
+
baseline = withoutNulls(baseline);
|
|
213
|
+
const baseShape = policyShapeErrors(baseline);
|
|
214
|
+
if (baseShape.length) return unreadable(`has wrongly typed fields (${baseShape.join("; ")})`);
|
|
215
|
+
const raw = await read(existsSync(path) ? await readFile(path, "utf8") : null);
|
|
216
|
+
const current = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? withoutNulls(raw) : raw;
|
|
217
|
+
// A wrongly typed field in the working tree is read by the hooks as its default, silently; it is reported instead.
|
|
218
|
+
const shape = typeof current === "object" && current !== null && !Array.isArray(current) ? policyShapeErrors(current) : ["the document is not a policy mapping"];
|
|
219
|
+
if (shape.length) return [{ file: "atdd-bun.yaml", detail: `has wrongly typed fields, which the hooks would ignore or misread: ${shape.join("; ")}`, restore: "correct the field types in atdd-bun.yaml, then re-run the check" }];
|
|
220
|
+
const loosened = loosenedPolicy(baseline, current);
|
|
182
221
|
return loosened.length ? [{ file: "atdd-bun.yaml", detail: `loosens the policy of ${against.slice(0, 7)}: ${loosened.join("; ")}`, restore: `git checkout ${against.slice(0, 7)} -- atdd-bun.yaml` }] : [];
|
|
183
222
|
}
|
|
184
223
|
|
|
@@ -12,7 +12,7 @@ A **tranche** is one independently mergeable piece of the program, on its own br
|
|
|
12
12
|
|
|
13
13
|
Its job is throughput: every worker slot busy, every tranche moving. It never implements, repairs tests, reviews, or merges a tranche.
|
|
14
14
|
|
|
15
|
-
1. Split the program into tranches with explicit dependencies, and write why the program exists, its scope and how it was split in `docs/delivery/index.adoc` (a docs-profile document). Activate a tranche as soon as its own dependencies have merged; do not wait for a whole wave. A tranche whose dependencies are still open may run PLAN and `plan_review` but nothing after; revalidate its plan once they merge.
|
|
15
|
+
1. Split the program into tranches with explicit dependencies, and write why the program exists, its scope and how it was split in `docs/delivery/index.adoc` (a docs-profile document; where the docs profile is active, `docs/index.adoc` is needed too, each with `:doc-id:` and `:status:`). Activate a tranche as soon as its own dependencies have merged; do not wait for a whole wave. A tranche whose dependencies are still open may run PLAN and `plan_review` but nothing after; revalidate its plan once they merge.
|
|
16
16
|
2. For each active tranche, create a worktree from the owning repository's workspace and start a driver in it through the multiplexer (see Multiplexer). Send the mandate, submit it, wait 4–6 s, and read the pane: a working indicator or agent output means it landed; an empty prompt or placeholder means retry before waiting on anything.
|
|
17
17
|
3. Wait on the multiplexer's events, not polling loops, and on every driver at once. When a slot frees, give it to the next ready tranche or to planning ahead.
|
|
18
18
|
4. Keep provider health for the whole program. When a driver reports a model unavailable, tell every driver to go straight to the next model in its lists until it recovers, so no tranche spends time rediscovering an outage.
|