@afokapu/atdd-bun 0.10.26 → 0.10.27

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.
@@ -51,10 +51,12 @@ content:
51
51
  fix_hint: |
52
52
  Regenerate the registry rather than editing it:
53
53
 
54
- atdd docs build # once #1623 lands the operator verbs (spec 2 §7)
54
+ atdd-bun docs adr-register # rewrites the adr-register region
55
+ atdd-bun docs adr-register --check # reports drift without writing
55
56
 
56
- Until then the capability reports the delta and the registry is regenerated by the same code
57
- path the check uses, so the two can never disagree. If you find yourself typing an entry into
57
+ It writes only between `// BEGIN GENERATED: adr-register` and `// END GENERATED: adr-register`
58
+ in decisions/index.adoc (add the two markers where the list belongs), one line per ADR by
59
+ `:adr-id:`, from the same documents the check reads. If you find yourself typing an entry into
58
60
  `decisions/index.adoc`, the entry belongs in an ADR file with a `:decides:` attribute instead.
59
61
  exceptions:
60
62
  - Prose surrounding the derived entries in `index.adoc` (a title, an introductory paragraph) is not
package/integrity.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.10.26",
2
+ "version": "0.10.27",
3
3
  "files": {
4
4
  "HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
5
5
  "PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
@@ -103,7 +103,7 @@
103
103
  "conventions/delivery/delivery.review.convention.yaml": "b033579c2d90fcb098d325dcc07c7b084e048cca43fbe8e28159ec08a412018a",
104
104
  "conventions/delivery/delivery.reviewer-independent.convention.yaml": "8c436d66ef5b1f6d6bc5dd55a235caca2490020e88665eecadb2ca41cd4149c5",
105
105
  "conventions/delivery/delivery.stages-complete.convention.yaml": "0e5dd8057ad51eff7906e3424a9686a81be1814447afb9a30acab43d39e49b4c",
106
- "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "0ea4232beba90280c664f93d097cafa44c67c2919c1468d4823fdc9e686aa5d2",
106
+ "conventions/planner.docs/planner.docs.adr-registry-derived.convention.yaml": "152cfb05c4a0e2053b09c05186d48ef330f4a94c5faddb8fbb322a81341a60a5",
107
107
  "conventions/planner.docs/planner.docs.area-index-required.convention.yaml": "fd931e9f225bfbb5e78436d0d8a2a89cc0f5114493579962c72dd971301becce",
108
108
  "conventions/planner.docs/planner.docs.artifact-path-shape.convention.yaml": "a9f6178d29abe9385342a46a2c57470a3631aaf5e26f01e3660b7088b042418e",
109
109
  "conventions/planner.docs/planner.docs.asciidoc-only.convention.yaml": "9e6fadbbe9aaf6015afc479f5b392e4c4324bfc47e1c59e315d26c2e5180a5f8",
@@ -1196,11 +1196,11 @@
1196
1196
  "relationships.yaml": "1cb713364aff54fb6e299e0737fe75a5d7b2e1dbd0a79b16b793aa1d269207b5",
1197
1197
  "src/agent.ts": "65e4579adb5a54977f557f8c783aba0e2a2a1d5cf3cea9f017b67ffe598b2f65",
1198
1198
  "src/board-ui.ts": "16a843705552f8f18e0e328e90e4a2a9148d771ca69f2708fe4a91302947dfad",
1199
- "src/board.ts": "72893289118461fbcee460d43f91e93fe4b078cabf171c03152c447eb8b71bb9",
1199
+ "src/board.ts": "bc70e479874f2f05a44293226e53eb9e763b661bdcebade4f8948b0dd2ee2ead",
1200
1200
  "src/ci.ts": "67e54d2cbfc44a9e42837d5e750af526d6255c539d3c452a4d5fca3226c0d9ac",
1201
- "src/cli.ts": "c1a7becd490ef9bf024569a869abc3b62ef48c0382897455e40fac9142d5ce46",
1201
+ "src/cli.ts": "e5ab4d2435fdcbebf462b4c2b67b495bd975d5deb668b088d8cba04a9d8b78a5",
1202
1202
  "src/delivery.ts": "f6bb6a7c42693d795b964c77b6186e1ca3a1556fd43932911a0ad42e72e7e441",
1203
- "src/docs-capability.ts": "115cf27049a5cf19c133bac5ec237072be6f969a59a2ef6767e054108a21dc5c",
1203
+ "src/docs-capability.ts": "8a7a4360976773fe462b1860eaf26182b016bcb1fbcd6a6d5423bd81379dbe67",
1204
1204
  "src/enforce.ts": "41a6f8c058a2a034996c817f224191403cc66ccc4ed4e04bbefc430014c783d8",
1205
1205
  "src/hooks.ts": "2feeec2674c1462c94256de02ce59ccc6e3ff671ec4dca1bf9903c32888b600b",
1206
1206
  "src/index.ts": "8ee4d7716794f6990ae8580e471b98256a0d0223dad063cd3b4542e598b6e744",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-bun",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/afokapu/atdd-bun.git"
package/src/board.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { join } from "node:path";
@@ -132,6 +132,16 @@ function cursorFile(agent: string, topics: string[], env: Record<string, string
132
132
  return join(env.ATDD_BOARD_STATE || join(homedir(), ".atdd-board", "cursors"), key);
133
133
  }
134
134
  const loadCursor = (file: string) => { try { return readFileSync(file, "utf8").trim() || undefined; } catch { return undefined; } };
135
+ /** The place to start from on a topic set this identity has never listened to: its OLDEST saved place on any other set,
136
+ * so nothing unread is skipped and nothing older is replayed. Without one, the start of the board, as before. */
137
+ function seedCursor(agent: string, env: Record<string, string | undefined>): string | undefined {
138
+ const dir = env.ATDD_BOARD_STATE || join(homedir(), ".atdd-board", "cursors"), prefix = `${agent}__`.replace(/[^A-Za-z0-9@+_.-]/g, "_");
139
+ try {
140
+ const saved = readdirSync(dir).filter(name => name.startsWith(prefix)).map(name => ({ file: join(dir, name), at: statSync(join(dir, name)).mtimeMs })).sort((a, b) => a.at - b.at);
141
+ for (const { file } of saved) { const id = loadCursor(file); if (id) return id; }
142
+ } catch { /* no saved places */ }
143
+ return undefined;
144
+ }
135
145
  const saveCursor = (file: string, id: string) => { try { mkdirSync(join(file, ".."), { recursive: true }); writeFileSync(file, `${id}\n`); } catch { /* best effort */ } };
136
146
 
137
147
  export function format(message: Message, withTopic = false): string {
@@ -159,7 +169,7 @@ export async function chat(args: string[], env: Record<string, string | undefine
159
169
  if (command === "wait") {
160
170
  // Without --since, continue from where this identity last stopped on these topics; either way, remember the place.
161
171
  const file = cursorFile(me, topics, env);
162
- const message = await waitFor(url, topics, me, flag("since") || loadCursor(file) || "all", Number(flag("timeout") ?? 0), list(flag("skip")), 500, id => saveCursor(file, id));
172
+ const message = await waitFor(url, topics, me, flag("since") || loadCursor(file) || seedCursor(me, env) || "all", Number(flag("timeout") ?? 0), list(flag("skip")), 500, id => saveCursor(file, id));
163
173
  if (!message) { console.error(`no message for ${me} on ${topics.join(", ")} yet; run the same wait again to continue`); return 2; }
164
174
  console.log(format(message, topics.length > 1));
165
175
  return 0;
@@ -169,7 +179,7 @@ export async function chat(args: string[], env: Record<string, string | undefine
169
179
  // this identity, as it arrives, until --timeout (seconds; none = until stopped). Same place-keeping as wait.
170
180
  const file = cursorFile(me, topics, env), skip = list(flag("skip")), total = Number(flag("timeout") ?? 0);
171
181
  const deadline = total > 0 ? Date.now() + total * 1000 : Infinity;
172
- let since = flag("since") || loadCursor(file) || "all";
182
+ let since = flag("since") || loadCursor(file) || seedCursor(me, env) || "all";
173
183
  for (;;) {
174
184
  const left = Math.ceil((deadline - Date.now()) / 1000);
175
185
  if (left <= 0) return 0;
package/src/cli.ts CHANGED
@@ -5,6 +5,7 @@ import { ciInit, ciStatus } from "./ci";
5
5
  import { agentInit, agentStatus } from "./agent";
6
6
  import { checkIntegrity, formatIntegrity, integrityInit, integrityStatus } from "./integrity";
7
7
  import { journeyDocs } from "./journey-docs";
8
+ import { adrRegister } from "./docs-capability";
8
9
  import { releaseCheck } from "./release";
9
10
  import { initializeRepository } from "./setup";
10
11
  import { chat } from "./board";
@@ -21,6 +22,7 @@ const usage = {
21
22
  "atdd-bun agent <init|status> [--replace]",
22
23
  "atdd-bun integrity [init|status] [--replace]",
23
24
  "atdd-bun docs journeys [--out <dir>] [--check] [--force]",
25
+ "atdd-bun docs adr-register [--check]",
24
26
  "atdd-bun release check",
25
27
  "atdd-bun chat topic <program> [<tranche> [<stage> <round>]]",
26
28
  "atdd-bun chat post <topic> --to <agent,...> [--kind K] [--stage S] [--reply-to ID] [--repo R] [--branch B] [--worktree W] [--goal G] (body on stdin)",
@@ -77,8 +79,12 @@ if (args[0] === "ci") {
77
79
  const result = args[1] === "init" ? await ciInit(process.cwd(), args.includes("--replace")) : args[1] === "status" ? await ciStatus() : fail("ci requires init or status");
78
80
  console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
79
81
  }
82
+ if (args[0] === "docs" && args[1] === "adr-register") {
83
+ const result = await adrRegister(process.cwd(), args.includes("--check"));
84
+ console[result.ok ? "log" : "error"](result.message); process.exit(result.ok ? 0 : 1);
85
+ }
80
86
  if (args[0] === "docs") {
81
- if (args[1] !== "journeys") fail("docs requires journeys");
87
+ if (args[1] !== "journeys") fail("docs requires journeys or adr-register");
82
88
  const at = args.indexOf("--out"), out = at === -1 ? undefined : args[at + 1];
83
89
  if (at !== -1 && !out) fail("--out requires a directory");
84
90
  const result = await journeyDocs({ out, check: args.includes("--check"), force: args.includes("--force") });
@@ -184,3 +184,26 @@ export async function checkDocumentation(input: { root: string; declaration: Doc
184
184
  try { return await checkDocumentationInner(input); }
185
185
  catch (error) { return { verdict: "FAIL", findings: [{ rule_id: null, where: input.root, message: `the documentation capability raised ${error instanceof Error ? error.name : "Error"}: ${String(error)}. A capability that crashes has not discharged the obligation.` }], checked: [] }; }
186
186
  }
187
+
188
+ const ADR_REGION = /^(\/\/\s*BEGIN GENERATED:\s*adr-register\b[^\n]*\n)[\s\S]*?(^\/\/\s*END GENERATED:\s*adr-register\b[^\n]*$)/m;
189
+
190
+ /** Regenerates the ADR register: the `// BEGIN GENERATED: adr-register` … `// END GENERATED: adr-register` region of
191
+ * docs/architecture/decisions/index.adoc, one line per ADR by :adr-id:, which planner.docs.adr-registry-derived
192
+ * requires to be projected, never typed. `check` reports drift without writing. Without the markers it writes nothing
193
+ * and says where they go: where the list belongs in the index is the corpus's choice. */
194
+ export async function adrRegister(root = process.cwd(), check = false): Promise<{ ok: boolean; message: string }> {
195
+ const index = join(root, ADR_INDEX);
196
+ if (!existsSync(index)) return { ok: false, message: `${ADR_INDEX} does not exist; create it with the adr-register markers where the list belongs` };
197
+ const text = await readFile(index, "utf8");
198
+ if (!ADR_REGION.test(text)) return { ok: false, message: `${ADR_INDEX} has no adr-register region; add "// BEGIN GENERATED: adr-register" and "// END GENERATED: adr-register" on their own lines where the list belongs` };
199
+ const adr = (await documents(root)).filter(d => d.path.startsWith(`${ADR_DIR}/`) && /^adr-\d{8}-\d{3}-[a-z0-9][a-z0-9-]*\.adoc$/.test(basename(d.path)) && d.attrs["adr-id"]);
200
+ const lines = adr.sort((a, b) => a.attrs["adr-id"].localeCompare(b.attrs["adr-id"])).map(d => {
201
+ const title = d.text.match(/^=\s+(.+)$/m)?.[1]?.trim() ?? d.attrs["adr-id"], status = d.attrs.status ? ` (${d.attrs.status})` : "";
202
+ return `* xref:${basename(d.path)}[${d.attrs["adr-id"]}] ${title}${status}`;
203
+ });
204
+ const next = text.replace(ADR_REGION, (_m, begin: string, end: string) => `${begin}${lines.join("\n")}${lines.length ? "\n" : ""}${end}`);
205
+ if (next === text) return { ok: true, message: `ADR register is current (${lines.length} ADRs)` };
206
+ if (check) return { ok: false, message: `ADR register has drifted; run atdd-bun docs adr-register` };
207
+ await Bun.write(index, next);
208
+ return { ok: true, message: `ADR register regenerated (${lines.length} ADRs)` };
209
+ }