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.
- package/bin/cli.js +3 -2
- package/package.json +1 -1
- package/template/.claude/agents/e2e-runner.md +44 -13
- package/template/.claude/agents/implementer.md +13 -7
- package/template/.claude/agents/infra-provisioner.md +33 -2
- package/template/.claude/agents/planner.md +79 -6
- package/template/.claude/agents/researcher.md +19 -8
- package/template/.claude/agents/spec-author.md +118 -0
- package/template/.claude/agents/test-author.md +76 -43
- package/template/.claude/anpunkit-manifest.json +47 -27
- package/template/.claude/commands/infra.md +28 -3
- package/template/.claude/commands/overview.md +84 -24
- package/template/.claude/commands/phase.md +139 -40
- package/template/.claude/commands/replan.md +23 -9
- package/template/.cursor/commands/infra.md +26 -1
- package/template/.cursor/commands/overview.md +83 -23
- package/template/.cursor/commands/phase.md +138 -39
- package/template/.cursor/commands/replan.md +22 -8
- package/template/.gitattributes +1 -0
- package/template/AGENTS.md +215 -36
- package/template/CLAUDE.md +1 -0
- package/template/README.md +59 -23
- package/template/anpunkit.png +0 -0
- package/template/commands.src/infra.md +28 -3
- package/template/commands.src/overview.md +84 -24
- package/template/commands.src/phase.md +139 -40
- package/template/commands.src/replan.md +23 -9
- package/template/docs/DESIGN_LOG.md +192 -1
- package/template/index.html +339 -0
- package/template/scripts/spec-conformance.sh +93 -0
- package/template/scripts/spec-staleness.sh +115 -0
- package/template/setup.sh +110 -47
- package/template/tests/helpers/spec-assert.py +162 -0
- 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
|
+
}
|