@afokapu/atdd-bun 0.10.32 → 0.10.34
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 +14 -10
- package/conventions/_profiles/architecture.yaml +190 -0
- package/conventions/_profiles/coder.yaml +804 -0
- package/conventions/_profiles/design.yaml +219 -0
- package/conventions/_profiles/docs.yaml +136 -0
- package/conventions/_profiles/htmx.yaml +436 -0
- package/conventions/_profiles/interlocking.yaml +248 -0
- package/conventions/_profiles/metrics.yaml +80 -0
- package/conventions/_profiles/planner.yaml +789 -0
- package/conventions/_profiles/runtime.yaml +62 -0
- package/conventions/_profiles/security.yaml +70 -0
- package/conventions/_profiles/telemetry.yaml +196 -0
- package/conventions/_profiles/tester.yaml +734 -0
- package/conventions/_profiles/topology.yaml +192 -0
- package/conventions/_profiles/traceability.yaml +94 -0
- package/conventions/_profiles/workflow.yaml +7 -0
- package/conventions/atdd-bun.profiles/atdd-bun.profiles.presets.convention.yaml +24 -2
- package/integrity.json +27 -12
- package/package.json +2 -1
- package/src/agent.ts +15 -15
- package/src/cli.ts +7 -0
- package/src/enforce.ts +23 -10
- package/src/hooks.ts +3 -3
- package/src/index.ts +1 -0
- package/src/integrity.ts +0 -2
- package/src/profile-registry.ts +78 -0
- package/src/setup.ts +4 -4
- package/templates/agents/AGENTS.block.md +4 -6
- package/templates/agents/atdd/SKILL.md +0 -20
package/src/index.ts
CHANGED
|
@@ -17,5 +17,6 @@ export type { Gap, Journey, JourneyModel, JourneyPath } from "./journey-docs";
|
|
|
17
17
|
export { checkIntegrity, checkInstalledPackage, formatIntegrity, integrityInit, loosenedPolicy, writeManifest } from "./integrity";
|
|
18
18
|
export type { IntegrityFinding, IntegrityOptions } from "./integrity";
|
|
19
19
|
export { releaseCheck } from "./release";
|
|
20
|
+
export { profileRegistries, renderProfileRegistries, PROFILE_REGISTRY_DIR } from "./profile-registry";
|
|
20
21
|
export { defaultTopology, topologyFor } from "./topology";
|
|
21
22
|
export type { Topology } from "./topology";
|
package/src/integrity.ts
CHANGED
|
@@ -18,7 +18,6 @@ const PACKAGE = "@afokapu/atdd-bun";
|
|
|
18
18
|
const ownRoot = resolve(import.meta.dir, "..");
|
|
19
19
|
export const MANIFEST = "integrity.json";
|
|
20
20
|
export const TEST_FILE = "atdd-bun.integrity.test.ts";
|
|
21
|
-
const SKILLS = [".agents/skills/atdd/SKILL.md", ".claude/skills/atdd/SKILL.md"];
|
|
22
21
|
const WORKFLOW = ".github/workflows/atdd-bun.yml";
|
|
23
22
|
const BLOCK = /<!-- atdd-bun:start[\s\S]*?<!-- atdd-bun:end -->/;
|
|
24
23
|
// Generated files carry the version that wrote them; an upgrade must not read as tampering.
|
|
@@ -99,7 +98,6 @@ async function checkGenerated(root: string, packageRoot: string, skipWorkflow =
|
|
|
99
98
|
if (skipWorkflow) { /* rendered from atdd-bun.yaml, which does not parse: reported by the caller */ }
|
|
100
99
|
else if (!existsSync(workflow)) findings.push({ file: WORKFLOW, detail: "is missing", restore: "bun run atdd-bun ci init --replace" });
|
|
101
100
|
else if (unstamp(await readFile(workflow, "utf8")) !== unstamp(await renderWorkflow(root))) findings.push({ file: WORKFLOW, detail: "was edited; it must match what the package generates (protected_branches decides its push branches)", restore: "bun run atdd-bun ci init --replace" });
|
|
102
|
-
for (const skill of SKILLS) await same(skill, "templates/agents/atdd/SKILL.md", "bun run atdd-bun agent init --replace");
|
|
103
101
|
await same(relative(root, await testFilePath(root)), "templates/agents/atdd-bun.integrity.test.ts", "bun run atdd-bun integrity init --replace");
|
|
104
102
|
const canonical = (await readFile(join(packageRoot, "templates/agents/AGENTS.block.md"), "utf8")).match(BLOCK)![0];
|
|
105
103
|
for (const file of instructionPaths) {
|
|
@@ -0,0 +1,78 @@
|
|
|
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, profileConventions, 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.length ? "implementations:" : "implementations: []", ...implementations.map(implementation => ` - ${scalar(implementation)}`),
|
|
40
|
+
conventions.length ? "conventions:" : "conventions: []", ...conventions.flatMap(convention => [` - rule_id: ${scalar(convention.rule_id)}`, ` path: ${scalar(convention.path)}`]),
|
|
41
|
+
relationships.length ? "relationships:" : "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 supplementary = profileConventions[profile];
|
|
57
|
+
const duplicate = supplementary.find(convention => conventions.has(convention.rule_id) || ruleIds.includes(convention.rule_id));
|
|
58
|
+
if (duplicate) throw new Error(`${profile} duplicates convention ${duplicate.rule_id}`);
|
|
59
|
+
const selectedConventions = [...ruleIds.map(id => conventions.get(id)!), ...supplementary];
|
|
60
|
+
const rules = new Set(selectedConventions.map(convention => convention.rule_id));
|
|
61
|
+
const relationships = sorted(graph.edges.filter(edge => rules.has(edge.source_ref) || rules.has(edge.target_ref)), (left, right) => relationshipKey(left).localeCompare(relationshipKey(right)));
|
|
62
|
+
rendered.set(`${PROFILE_REGISTRY_DIR}/${profile}.yaml`, render(profile, implementations, selectedConventions, relationships));
|
|
63
|
+
}
|
|
64
|
+
return rendered;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Write or check the shipped profile registries. `check` reports every stale or unexpected generated file. */
|
|
68
|
+
export async function profileRegistries(options: { root?: string; check?: boolean } = {}) {
|
|
69
|
+
const root = resolve(options.root ?? resolve(import.meta.dir, "..")), expected = await renderProfileRegistries(root), directory = join(root, PROFILE_REGISTRY_DIR);
|
|
70
|
+
const actual = existsSync(directory) ? sorted((await readdir(directory)).filter(name => name.endsWith(".yaml")).map(name => `${PROFILE_REGISTRY_DIR}/${name}`)) : [];
|
|
71
|
+
const files: string[] = [];
|
|
72
|
+
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);
|
|
73
|
+
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 };
|
|
74
|
+
await mkdir(directory, { recursive: true });
|
|
75
|
+
for (const path of actual.filter(path => !expected.has(path))) await rm(join(root, path));
|
|
76
|
+
for (const [path, content] of expected) await writeFile(join(root, path), content);
|
|
77
|
+
return { ok: true, message: `wrote ${expected.size} profile registries`, files: sorted(expected.keys()) };
|
|
78
|
+
}
|
package/src/setup.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ciInit, ciStatus } from "./ci";
|
|
|
2
2
|
import { hooksStatus, installHooks } from "./hooks";
|
|
3
3
|
import { agentInit, agentStatus } from "./agent";
|
|
4
4
|
import { integrityInit, integrityStatus } from "./integrity";
|
|
5
|
-
import {
|
|
5
|
+
import { defaultProfiles } from "./enforce";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { writeFile } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
@@ -14,7 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
export async function policyInit(root = process.cwd()) {
|
|
15
15
|
const file = join(root, "atdd-bun.yaml");
|
|
16
16
|
if (existsSync(file)) return { ok: true, message: `${file} kept` };
|
|
17
|
-
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${
|
|
17
|
+
await writeFile(file, `# Generated by atdd-bun init. The profiles this repository enforces; trim the list before the first commit to\n# adopt gradually. Once committed, removing a profile or the list is reported by the integrity check.\nprofiles: [${defaultProfiles.join(", ")}]\n`);
|
|
18
18
|
return { ok: true, message: file };
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -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
|
|
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
|
|
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
|
|
2
|
+
## ATDD
|
|
3
3
|
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|