create-anpunkit 2.0.3 → 2.2.0

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.
Files changed (34) hide show
  1. package/bin/cli.js +3 -2
  2. package/package.json +1 -1
  3. package/template/.claude/agents/e2e-runner.md +44 -13
  4. package/template/.claude/agents/implementer.md +13 -7
  5. package/template/.claude/agents/infra-provisioner.md +33 -2
  6. package/template/.claude/agents/planner.md +79 -6
  7. package/template/.claude/agents/researcher.md +19 -8
  8. package/template/.claude/agents/spec-author.md +118 -0
  9. package/template/.claude/agents/test-author.md +76 -43
  10. package/template/.claude/anpunkit-manifest.json +47 -27
  11. package/template/.claude/commands/infra.md +28 -3
  12. package/template/.claude/commands/overview.md +84 -24
  13. package/template/.claude/commands/phase.md +139 -40
  14. package/template/.claude/commands/replan.md +23 -9
  15. package/template/.cursor/commands/infra.md +26 -1
  16. package/template/.cursor/commands/overview.md +83 -23
  17. package/template/.cursor/commands/phase.md +138 -39
  18. package/template/.cursor/commands/replan.md +22 -8
  19. package/template/.gitattributes +1 -0
  20. package/template/AGENTS.md +215 -36
  21. package/template/CLAUDE.md +1 -0
  22. package/template/README.md +59 -23
  23. package/template/anpunkit.png +0 -0
  24. package/template/commands.src/infra.md +28 -3
  25. package/template/commands.src/overview.md +84 -24
  26. package/template/commands.src/phase.md +139 -40
  27. package/template/commands.src/replan.md +23 -9
  28. package/template/docs/DESIGN_LOG.md +192 -1
  29. package/template/index.html +339 -0
  30. package/template/scripts/spec-conformance.sh +93 -0
  31. package/template/scripts/spec-staleness.sh +115 -0
  32. package/template/setup.sh +110 -47
  33. package/template/tests/helpers/spec-assert.py +162 -0
  34. package/template/tests/helpers/spec-assert.ts +158 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * spec-assert.ts — kit-versioned matcher-aware deep-equality comparator (v2.2).
3
+ *
4
+ * The generated `data`/`ui` boundary harness asserts an actual value against
5
+ * `fixtures/<case-id>-expected.json` via `specAssert(actual, expected)`. Deep
6
+ * equality, EXCEPT that volatile fields in the expected fixture carry a matcher
7
+ * token instead of a literal (§5.49). This is the ONLY comparator — no per-project
8
+ * assertion logic. One file per supported test language; this is the TS/JS one.
9
+ *
10
+ * Matcher tokens (string values in the expected fixture):
11
+ *
12
+ * "<UUID>" any UUID v4 string
13
+ * "<ISO8601>" any ISO 8601 datetime string
14
+ * "<ANY_STRING>" any string
15
+ * "<ANY_NUMBER>" any finite number
16
+ * "<UNORDERED>" any array (presence + shape only)
17
+ * "<MATCHES:regex>" any string matching the pattern (RegExp.test)
18
+ *
19
+ * Order-insensitive array WITH item checking: wrap the expected array as
20
+ * { "<UNORDERED>": [item, item, ...] }
21
+ * and the actual must be an array that is an order-insensitive deep-equal multiset.
22
+ *
23
+ * A token asserts PRESENCE + SHAPE — never "ignore this field". Missing volatile
24
+ * fields fail (strict key sets); extra actual keys also fail.
25
+ *
26
+ * import { specAssert, loadFixture } from "../helpers/spec-assert";
27
+ * specAssert(actual, loadFixture("fixtures/PH2-ORDER-01-expected.json"));
28
+ */
29
+
30
+ import { readFileSync } from "node:fs";
31
+
32
+ const UUID4 =
33
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
34
+ const ISO8601 =
35
+ /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:?\d{2})?$/;
36
+ const MATCHES_PREFIX = "<MATCHES:";
37
+
38
+ export class SpecMismatch extends Error {
39
+ constructor(message: string) {
40
+ super(message);
41
+ this.name = "SpecMismatch";
42
+ }
43
+ }
44
+
45
+ export function loadFixture<T = unknown>(path: string): T {
46
+ return JSON.parse(readFileSync(path, "utf-8")) as T;
47
+ }
48
+
49
+ function fail(path: string, msg: string): never {
50
+ throw new SpecMismatch(`spec-assert mismatch at ${path}: ${msg}`);
51
+ }
52
+
53
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
54
+ return typeof v === "object" && v !== null && !Array.isArray(v);
55
+ }
56
+
57
+ function typeName(v: unknown): string {
58
+ if (v === null) return "null";
59
+ if (Array.isArray(v)) return "array";
60
+ return typeof v;
61
+ }
62
+
63
+ /** Returns true if `token` is a known matcher and `actual` satisfies it.
64
+ * Throws on a known token that is NOT satisfied. Returns false if not a token. */
65
+ function matchToken(actual: unknown, token: string, path: string): boolean {
66
+ switch (token) {
67
+ case "<UUID>":
68
+ if (!(typeof actual === "string" && UUID4.test(actual)))
69
+ fail(path, `expected a UUID v4 string, got ${JSON.stringify(actual)}`);
70
+ return true;
71
+ case "<ISO8601>":
72
+ if (!(typeof actual === "string" && ISO8601.test(actual)))
73
+ fail(path, `expected an ISO 8601 datetime string, got ${JSON.stringify(actual)}`);
74
+ return true;
75
+ case "<ANY_STRING>":
76
+ if (typeof actual !== "string")
77
+ fail(path, `expected any string, got ${typeName(actual)}`);
78
+ return true;
79
+ case "<ANY_NUMBER>":
80
+ if (typeof actual !== "number" || !Number.isFinite(actual))
81
+ fail(path, `expected any finite number, got ${JSON.stringify(actual)}`);
82
+ return true;
83
+ case "<UNORDERED>":
84
+ if (!Array.isArray(actual))
85
+ fail(path, `expected any array, got ${typeName(actual)}`);
86
+ return true;
87
+ }
88
+ if (token.startsWith(MATCHES_PREFIX) && token.endsWith(">")) {
89
+ const pattern = token.slice(MATCHES_PREFIX.length, -1);
90
+ if (!(typeof actual === "string" && new RegExp(pattern).test(actual)))
91
+ fail(path, `expected a string matching /${pattern}/, got ${JSON.stringify(actual)}`);
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ function unorderedEqual(actual: unknown, items: unknown[], path: string): void {
98
+ if (!Array.isArray(actual))
99
+ fail(path, `expected an array (unordered), got ${typeName(actual)}`);
100
+ if (actual.length !== items.length)
101
+ fail(path, `array length ${actual.length} != expected ${items.length} (unordered)`);
102
+ const remaining = [...actual];
103
+ items.forEach((exp, i) => {
104
+ let found = -1;
105
+ for (let j = 0; j < remaining.length; j++) {
106
+ try {
107
+ specAssert(remaining[j], exp, `${path}[unordered:${i}]`);
108
+ found = j;
109
+ break;
110
+ } catch (e) {
111
+ if (e instanceof SpecMismatch) continue;
112
+ throw e;
113
+ }
114
+ }
115
+ if (found < 0) fail(path, `no actual element matches expected item #${i}: ${JSON.stringify(exp)}`);
116
+ remaining.splice(found, 1);
117
+ });
118
+ }
119
+
120
+ /** Assert `actual` deep-equals `expected`, honoring matcher tokens. Throws
121
+ * SpecMismatch with a JSON path on the first divergence. */
122
+ export function specAssert(actual: unknown, expected: unknown, path = "$"): void {
123
+ // scalar matcher token
124
+ if (typeof expected === "string" && matchToken(actual, expected, path)) return;
125
+
126
+ // { "<UNORDERED>": [...] } wrapper → order-insensitive item compare
127
+ if (isPlainObject(expected)) {
128
+ const keys = Object.keys(expected);
129
+ if (keys.length === 1 && keys[0] === "<UNORDERED>") {
130
+ unorderedEqual(actual, expected["<UNORDERED>"] as unknown[], path);
131
+ return;
132
+ }
133
+ if (!isPlainObject(actual)) fail(path, `expected object, got ${typeName(actual)}`);
134
+ const expKeys = new Set(keys);
135
+ const actKeys = new Set(Object.keys(actual));
136
+ const missing = [...expKeys].filter((k) => !actKeys.has(k));
137
+ const extra = [...actKeys].filter((k) => !expKeys.has(k));
138
+ if (missing.length || extra.length)
139
+ fail(path, `key mismatch (missing=${JSON.stringify(missing)}, extra=${JSON.stringify(extra)})`);
140
+ for (const k of keys) specAssert(actual[k], expected[k], `${path}.${k}`);
141
+ return;
142
+ }
143
+
144
+ if (Array.isArray(expected)) {
145
+ if (!Array.isArray(actual)) fail(path, `expected array, got ${typeName(actual)}`);
146
+ if (actual.length !== expected.length)
147
+ fail(path, `array length ${actual.length} != expected ${expected.length}`);
148
+ for (let i = 0; i < expected.length; i++)
149
+ specAssert(actual[i], expected[i], `${path}[${i}]`);
150
+ return;
151
+ }
152
+
153
+ // scalar literal — strict equality (typeof must agree; NaN-safe)
154
+ if (typeName(actual) !== typeName(expected))
155
+ fail(path, `type ${typeName(actual)} != expected ${typeName(expected)}`);
156
+ if (actual !== expected && !(Number.isNaN(actual) && Number.isNaN(expected)))
157
+ fail(path, `${JSON.stringify(actual)} != expected ${JSON.stringify(expected)}`);
158
+ }