@afokapu/atdd-bun 0.10.32 → 0.10.33

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.
@@ -0,0 +1,74 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { concreteProfiles, declaredRuleIds, profileImplementations } from "./enforce";
5
+
6
+ export const PROFILE_REGISTRY_DIR = "conventions/_profiles";
7
+ type Convention = { rule_id: string; path: string };
8
+ type Relationship = Record<string, unknown> & { source_ref: string; target_ref: string; type: string };
9
+ type Graph = { edges: Relationship[] };
10
+
11
+ async function filesBelow(root: string, extension: string): Promise<string[]> {
12
+ const out: string[] = [];
13
+ for (const entry of await readdir(root, { withFileTypes: true })) {
14
+ const path = join(root, entry.name);
15
+ if (entry.isDirectory()) out.push(...await filesBelow(path, extension));
16
+ else if (entry.name.endsWith(extension)) out.push(path);
17
+ }
18
+ return out;
19
+ }
20
+
21
+ async function conventionIndex(root: string): Promise<Map<string, Convention>> {
22
+ const index = new Map<string, Convention>();
23
+ for (const base of ["planner-nodes/nodes", "conventions"]) for (const file of await filesBelow(join(root, base), ".convention.yaml")) {
24
+ const rule_id = (Bun.YAML.parse(await readFile(file, "utf8")) as { rule_id?: unknown }).rule_id;
25
+ if (typeof rule_id !== "string") continue;
26
+ const path = relative(root, file).replaceAll("\\", "/");
27
+ if (index.has(rule_id)) throw new Error(`multiple convention files declare ${rule_id}: ${index.get(rule_id)!.path}, ${path}`);
28
+ index.set(rule_id, { rule_id, path });
29
+ }
30
+ return index;
31
+ }
32
+
33
+ const sorted = <T>(items: Iterable<T>, compare: (left: T, right: T) => number = (left, right) => String(left).localeCompare(String(right))) => [...items].sort(compare);
34
+ const relationshipKey = (edge: Relationship) => `${edge.source_ref}\u0000${edge.target_ref}\u0000${edge.type}`;
35
+ const scalar = (value: unknown) => value === null ? "null" : typeof value === "string" ? JSON.stringify(value) : typeof value === "number" || typeof value === "boolean" ? String(value) : (() => { throw new Error(`profile registry cannot render non-scalar value ${JSON.stringify(value)}`); })();
36
+ const fields = (value: Record<string, unknown>, indent: string) => sorted(Object.entries(value), ([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${indent}${key}: ${scalar(entry)}`);
37
+ const render = (profile: string, implementations: string[], conventions: Convention[], relationships: Relationship[]) => [
38
+ 'schema_version: "1.0.0"', `profile: ${scalar(profile)}`,
39
+ "implementations:", ...implementations.map(implementation => ` - ${scalar(implementation)}`),
40
+ "conventions:", ...conventions.flatMap(convention => [` - rule_id: ${scalar(convention.rule_id)}`, ` path: ${scalar(convention.path)}`]),
41
+ "relationships:", ...relationships.flatMap(edge => { const entries = fields(edge, " "); return [` - ${entries[0]!.slice(4)}`, ...entries.slice(1)]; }), "",
42
+ ].join("\n");
43
+
44
+ /** Deterministically project profiles, detector manifests, conventions, and direct graph edges into small registries. */
45
+ export async function renderProfileRegistries(root = resolve(import.meta.dir, "..")): Promise<Map<string, string>> {
46
+ const conventions = await conventionIndex(root);
47
+ const graph = Bun.YAML.parse(await readFile(join(root, "relationships.yaml"), "utf8")) as Graph;
48
+ if (!Array.isArray(graph.edges)) throw new Error("relationships.yaml has no edges list");
49
+ const rendered = new Map<string, string>();
50
+ for (const profile of concreteProfiles) {
51
+ const implementations = sorted(profileImplementations[profile]);
52
+ const declared = await Promise.all(implementations.map(implementation => declaredRuleIds(implementation, join(root, "detectors"))));
53
+ const ruleIds = sorted(new Set(declared.flatMap(ids => [...ids])));
54
+ const missing = ruleIds.filter(id => !conventions.has(id));
55
+ if (missing.length) throw new Error(`${profile} declares rule IDs without exactly one convention: ${missing.join(", ")}`);
56
+ const rules = new Set(ruleIds);
57
+ const relationships = sorted(graph.edges.filter(edge => rules.has(edge.source_ref) || rules.has(edge.target_ref)), (left, right) => relationshipKey(left).localeCompare(relationshipKey(right)));
58
+ rendered.set(`${PROFILE_REGISTRY_DIR}/${profile}.yaml`, render(profile, implementations, ruleIds.map(id => conventions.get(id)!), relationships));
59
+ }
60
+ return rendered;
61
+ }
62
+
63
+ /** Write or check the shipped profile registries. `check` reports every stale or unexpected generated file. */
64
+ export async function profileRegistries(options: { root?: string; check?: boolean } = {}) {
65
+ const root = resolve(options.root ?? resolve(import.meta.dir, "..")), expected = await renderProfileRegistries(root), directory = join(root, PROFILE_REGISTRY_DIR);
66
+ const actual = existsSync(directory) ? sorted((await readdir(directory)).filter(name => name.endsWith(".yaml")).map(name => `${PROFILE_REGISTRY_DIR}/${name}`)) : [];
67
+ const files: string[] = [];
68
+ for (const path of sorted(new Set([...expected.keys(), ...actual]))) if (!expected.has(path) || !existsSync(join(root, path)) || (await readFile(join(root, path), "utf8")) !== expected.get(path)) files.push(path);
69
+ if (options.check) return { ok: !files.length, message: files.length ? `profile registries have drifted: ${files.join(", ")}; run: atdd-bun profiles registry` : "profile registries are current", files };
70
+ await mkdir(directory, { recursive: true });
71
+ for (const path of actual.filter(path => !expected.has(path))) await rm(join(root, path));
72
+ for (const [path, content] of expected) await writeFile(join(root, path), content);
73
+ return { ok: true, message: `wrote ${expected.size} profile registries`, files: sorted(expected.keys()) };
74
+ }
package/src/setup.ts CHANGED
@@ -33,10 +33,10 @@ export async function initializeRepository(root = process.cwd(), replace = false
33
33
 
34
34
  const existingAgent = await agentStatus(root);
35
35
  const agent = replace || !existingAgent.ok ? await agentInit(root, replace) : existingAgent;
36
- if (!agent.ok) return { ok: false, message: `agent skill: ${agent.message}` };
36
+ if (!agent.ok) return { ok: false, message: `agent instructions: ${agent.message}` };
37
37
 
38
38
  const existingIntegrity = await integrityStatus(root);
39
39
  const integrity = replace || !existingIntegrity.ok ? await integrityInit(root, replace) : existingIntegrity;
40
40
  if (!integrity.ok) return { ok: false, message: `integrity test: ${integrity.message}` };
41
- return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}\nagent skill: ${agent.message}\nintegrity test: ${integrity.message}\npolicy: ${policy.message}` };
41
+ return { ok: true, message: `hooks: ${hooks.message}\nCI workflow: ${ci.message}\nagent instructions: ${agent.message}\nintegrity test: ${integrity.message}\npolicy: ${policy.message}` };
42
42
  }
@@ -1,11 +1,9 @@
1
1
  <!-- atdd-bun:start — generated by @afokapu/atdd-bun {{VERSION}}; run `atdd-bun agent init --replace` to refresh -->
2
- ## ATDD lifecycle
2
+ ## ATDD
3
3
 
4
- Before changing code, tests, or `plan/`, follow `.agents/skills/atdd/SKILL.md`: PLAN → RED → GREEN → SMOKE → REFACTOR → TRACE, passing each stage's `atdd-bun` gate before starting the next.
4
+ For each task, read `conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml` in `node_modules/@afokapu/atdd-bun` and propose the governing profile(s). If the human has not selected them, ask them to choose; do not activate or deactivate profiles yourself. Then read the matching `conventions/_profiles/<profile>.yaml` registry and only the convention files it identifies as relevant.
5
5
 
6
- `atdd-bun.yaml` controls which profiles are active (`profiles:`; absent, every profile runs) and their settings. Turn a profile on or off only when the user asks. Adding one is always allowed; removing one loosens the gate, so the integrity check reports it until a human approves it on the base branch.
6
+ Follow the applicable conventions and their gates. Enter at the stage that fits the task; do not assume a linear lifecycle.
7
7
 
8
- When the work reaches a stage whose profiles are not active yet, ask the user what they want to do and propose the matching preset from the package convention `atdd-bun.profiles.presets`.
9
-
10
- Never modify the toolkit itself (`node_modules/@afokapu/atdd-bun`, or the files atdd-bun generates); change only the configuration it offers, and loosen it only when the user asks. The integrity test and CI fail if the toolkit is modified.
8
+ Never modify the toolkit itself (`node_modules/@afokapu/atdd-bun`) or its generated files. Change only its configuration, and loosen it only when the user asks.
11
9
  <!-- atdd-bun:end -->
@@ -1,20 +0,0 @@
1
- ---
2
- name: atdd
3
- description: Use before writing or changing code, tests, or plan/ files in this repository. New intent is decomposed and delivered through the ATDD lifecycle PLAN → RED → GREEN → SMOKE → REFACTOR → TRACE, each stage gated by atdd-bun. Skip PLAN only when the change fits an acceptance that already exists.
4
- ---
5
- <!-- Generated by @afokapu/atdd-bun {{VERSION}}. Do not edit; run `atdd-bun agent init --replace`. -->
6
-
7
- Conventions live in `node_modules/@afokapu/atdd-bun/` (`planner-nodes/nodes/`, `conventions/`). Read the ones a stage names; do not restate them. Finish each stage by passing its gate before starting the next.
8
-
9
- Technology and architecture choices already made are the accepted ADRs listed in `docs/architecture/decisions/index.adoc`. Follow them in PLAN and GREEN; a new choice gets its own ADR under `docs/architecture/decisions/` (`planner.docs.*`).
10
-
11
- 1. PLAN — Decompose the intent into wagon → WMBT → acceptance → train/interlocking → journey → contract under `plan/`, following `planner.decomposition.*`; every WMBT declares a SMOKE acceptance. Gate: `bun run atdd-bun planner`.
12
- 2. RED — For each acceptance, write a test headed `// URN: test:{wagon}:{feature}:{ACC-ID}` and `// Phase: RED` that fails for the missing behaviour (`tester.bun.red-*`). Gate: `bun run atdd-bun tester`.
13
- 3. GREEN — Write the least code that passes; each source file carries `URN: component:{wagon}:{feature}:{Name}:{side}:{layer}` and a `Tested-By:` block (`coder.bun.green-*`). Gate: `bun test`.
14
- 4. SMOKE — Prove the SMOKE acceptance through the real entry point with no mocks or spies, asserting only on observable output: HTTP, markup, stdout, exit code (`tester.bun.smoke-*`, `planner.smoke.*`). Gate: `bun run atdd-bun tester`.
15
- 5. REFACTOR — With tests green, reduce complexity, fix layering, and remove security faults until the rules pass, without changing behaviour (`coder.bun.complexity-*`, `quality-*`, `composition-*`, `design-*`, `security-*`). Gate: `bun run atdd-bun coder security`.
16
- 6. TRACE — Every acceptance has a test, every test resolves to a declared acceptance, every source file resolves to its tests. Gate: `bun run atdd-bun traceability`, then `bun run atdd-bun all`.
17
-
18
- When a gate fails, open `<rule_id>.convention.yaml` for the reported rule ID and fix the artifact. Never skip, suppress, or edit a convention to get green.
19
-
20
- Never edit `node_modules/@afokapu/atdd-bun`, the files atdd-bun generates (this skill, `AGENTS.md` block, `.github/workflows/atdd-bun.yml`, `atdd-bun.integrity.test.ts`), or loosen `atdd-bun.yaml`. `atdd-bun.integrity.test.ts` fails if you do, and CI repeats the check on a clean install. If one of them needs to change, stop and ask the human.