create-anpunkit 2.1.0 → 2.3.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 +2 -2
- package/package.json +2 -3
- package/template/.claude/agents/debugger.md +1 -1
- package/template/.claude/agents/e2e-runner.md +56 -29
- package/template/.claude/agents/implementer.md +14 -8
- package/template/.claude/agents/infra-provisioner.md +53 -59
- package/template/.claude/agents/planner.md +99 -30
- package/template/.claude/agents/researcher.md +16 -1
- package/template/.claude/agents/spec-author.md +126 -0
- package/template/.claude/agents/synthesizer.md +1 -1
- package/template/.claude/agents/test-author.md +81 -63
- package/template/.claude/anpunkit-manifest.json +66 -107
- package/template/.claude/commands/infra.md +7 -3
- package/template/.claude/commands/log-decision.md +1 -1
- package/template/.claude/commands/log-issue.md +1 -1
- package/template/.claude/commands/overview.md +55 -13
- package/template/.claude/commands/phase.md +153 -81
- package/template/.claude/commands/quick.md +1 -1
- package/template/.claude/commands/replan.md +1 -1
- package/template/.claude/commands/store-wisdom.md +1 -1
- package/template/.claude/commands/synthesize.md +1 -1
- package/template/.claude/commands/unstuck.md +1 -1
- package/template/.claude/hooks/session-start.sh +5 -11
- package/template/.claude/ref/compression.md +56 -0
- package/template/.claude/skills/karpathy-guidelines/SKILL.md +1 -1
- package/template/.gitattributes +1 -0
- package/template/AGENTS.md +264 -130
- package/template/CLAUDE.md +5 -9
- package/template/README.md +116 -136
- package/template/commands.src/infra.md +7 -3
- package/template/commands.src/log-decision.md +1 -1
- package/template/commands.src/log-issue.md +1 -1
- package/template/commands.src/overview.md +55 -13
- package/template/commands.src/phase.md +153 -81
- package/template/commands.src/quick.md +1 -1
- package/template/commands.src/replan.md +1 -1
- package/template/commands.src/store-wisdom.md +1 -1
- package/template/commands.src/synthesize.md +1 -1
- package/template/commands.src/unstuck.md +1 -1
- package/template/docs/DESIGN_LOG.md +414 -9
- package/template/knowledge/azure.md +161 -0
- package/template/knowledge/webapp.md +265 -0
- package/template/scripts/spec-conformance.sh +93 -0
- package/template/scripts/spec-staleness.sh +115 -0
- package/template/setup.sh +30 -111
- package/template/tests/helpers/spec-assert.py +162 -0
- package/template/tests/helpers/spec-assert.ts +158 -0
- package/template/.claude/hooks/cursor-session-start.sh +0 -17
- package/template/.claude/skills/caveman/SKILL.md +0 -39
- package/template/.cursor/commands/infra.md +0 -82
- package/template/.cursor/commands/log-decision.md +0 -29
- package/template/.cursor/commands/log-issue.md +0 -23
- package/template/.cursor/commands/overview.md +0 -145
- package/template/.cursor/commands/phase.md +0 -249
- package/template/.cursor/commands/quick.md +0 -25
- package/template/.cursor/commands/replan.md +0 -73
- package/template/.cursor/commands/store-wisdom.md +0 -191
- package/template/.cursor/commands/synthesize.md +0 -22
- package/template/.cursor/commands/unstuck.md +0 -36
- package/template/.cursor/hooks.json +0 -14
- package/template/.cursor/rules/anpunkit.md +0 -11
- package/template/anpunkit.png +0 -0
- package/template/docker-compose.test.yml +0 -34
- package/template/e2e/global-setup.ts +0 -57
- package/template/index.html +0 -340
- package/template/playwright.config.ts +0 -28
- package/template/scripts/auth-setup.sh +0 -51
- package/template/scripts/e2e-stack.sh +0 -65
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# knowledge/webapp.md — matured web-app practice (preinstalled)
|
|
2
|
+
|
|
3
|
+
> **What this file is.** Use-case knowledge for browser-facing web apps,
|
|
4
|
+
> consulted by `researcher` (STEP 0.5) when `knowledge/webapp.md` appears in
|
|
5
|
+
> the `knowledge_docs:` list of `docs/OVERVIEW.md`. Selection is deterministic
|
|
6
|
+
> — declared at `/overview`, never inferred (hard rule 10).
|
|
7
|
+
>
|
|
8
|
+
> **Templates below are materialized per-project** — copied to the stated path
|
|
9
|
+
> the first time the phase work needs them. Once materialized they are
|
|
10
|
+
> user-owned project files: adjust freely, the kit never overwrites them.
|
|
11
|
+
|
|
12
|
+
## Browser boundary practice (E2E_KIND: browser)
|
|
13
|
+
|
|
14
|
+
- `ui` spec cases carry a `fixtures/<case-id>-ui.json` descriptor with a
|
|
15
|
+
closed, kit-versioned assert vocabulary: `visible`, `text-equals`,
|
|
16
|
+
`enabled`, `count`. `e2e-runner` EMITS Playwright specs from descriptors —
|
|
17
|
+
it never authors blind from prose.
|
|
18
|
+
- Each emitted spec block carries a `// spec: <case-id>` citation
|
|
19
|
+
(`spec-conformance.sh` checks it).
|
|
20
|
+
- EVIDENCE (hard rule 13): at each UI-existence assertion, capture a
|
|
21
|
+
screenshot regardless of pass/fail to
|
|
22
|
+
`docs/evidence/e2e-phase-<n>/<case-id>-<element-slug>.png` — one shot per
|
|
23
|
+
asserted element, on green as well as red.
|
|
24
|
+
- `ui` cases have NO mock mirror — the browser IS the boundary.
|
|
25
|
+
|
|
26
|
+
## E2E stack ritual
|
|
27
|
+
|
|
28
|
+
Two target modes, recorded in `docs/INFRA.md`:
|
|
29
|
+
|
|
30
|
+
- **azure-deployed** (`E2E_STACK_EXTERNAL=1` in `.env.test`): tests hit the
|
|
31
|
+
deployed app; `e2e-stack.sh up` only health-checks reachability.
|
|
32
|
+
- **local-docker**: `e2e-stack.sh up` builds + starts the compose stack and
|
|
33
|
+
polls health.
|
|
34
|
+
|
|
35
|
+
Sequence, always: `bash scripts/e2e-stack.sh up` -> `npx playwright test` ->
|
|
36
|
+
`bash scripts/e2e-stack.sh down`. Stack didn't come up -> classify
|
|
37
|
+
**STACK NOT READY** (not a bug; no debug budget).
|
|
38
|
+
|
|
39
|
+
## Web deploy ritual (DEPLOY_KIND: cloud-deploy)
|
|
40
|
+
|
|
41
|
+
- Deploy task lives in the final phase (DEPLOY_KIND completion, hard rules).
|
|
42
|
+
- After deploy: record the deployed base URL in `docs/INFRA.md`
|
|
43
|
+
(`## Deployed base URL`) and confirm `docs/ENDPOINTS.md` entries resolve
|
|
44
|
+
against it; run a `/health` (or equivalent) check before calling it done.
|
|
45
|
+
|
|
46
|
+
## .gitignore lines a materialized stack needs
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
e2e/.auth/
|
|
50
|
+
playwright-report/
|
|
51
|
+
test-results/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Templates
|
|
57
|
+
|
|
58
|
+
### `playwright.config.ts` — materialize to project root
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// playwright.config.ts
|
|
62
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
63
|
+
|
|
64
|
+
export default defineConfig({
|
|
65
|
+
testDir: './e2e',
|
|
66
|
+
globalSetup: './e2e/global-setup.ts',
|
|
67
|
+
fullyParallel: false,
|
|
68
|
+
forbidOnly: !!process.env.CI,
|
|
69
|
+
retries: process.env.CI ? 1 : 0,
|
|
70
|
+
workers: 1,
|
|
71
|
+
reporter: [['list'], ['html', { open: 'never' }]],
|
|
72
|
+
use: {
|
|
73
|
+
baseURL: process.env.E2E_BASE_URL || 'http://localhost:8080',
|
|
74
|
+
storageState: 'e2e/.auth/state.json',
|
|
75
|
+
trace: 'retain-on-failure',
|
|
76
|
+
screenshot: 'only-on-failure',
|
|
77
|
+
actionTimeout: 15_000,
|
|
78
|
+
},
|
|
79
|
+
projects: [
|
|
80
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
81
|
+
],
|
|
82
|
+
webServer: process.env.E2E_STACK_EXTERNAL ? undefined : {
|
|
83
|
+
command: 'bash scripts/e2e-stack.sh up',
|
|
84
|
+
url: process.env.E2E_BASE_URL || 'http://localhost:8080',
|
|
85
|
+
timeout: 180_000,
|
|
86
|
+
reuseExistingServer: !process.env.CI,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### `e2e/global-setup.ts` — materialize to `e2e/`
|
|
92
|
+
|
|
93
|
+
Entra ID ROPC token injection (Azure/MSAL apps — see `knowledge/azure.md` for
|
|
94
|
+
the auth practice). Known gap: the localStorage keys (`e2e.access_token`,
|
|
95
|
+
`e2e.id_token`) are a stub convention — align them with your app's real MSAL
|
|
96
|
+
cache keys when materializing.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// e2e/global-setup.ts
|
|
100
|
+
import { chromium } from '@playwright/test';
|
|
101
|
+
import * as fs from 'fs';
|
|
102
|
+
import * as path from 'path';
|
|
103
|
+
|
|
104
|
+
const AUTH_DIR = path.join(__dirname, '.auth');
|
|
105
|
+
const STATE = path.join(AUTH_DIR, 'state.json');
|
|
106
|
+
|
|
107
|
+
async function fetchRopcToken() {
|
|
108
|
+
const tenant = reqEnv('E2E_TENANT_ID');
|
|
109
|
+
const body = new URLSearchParams({
|
|
110
|
+
grant_type: 'password',
|
|
111
|
+
client_id: reqEnv('E2E_CLIENT_ID'),
|
|
112
|
+
username: reqEnv('E2E_TEST_USER'),
|
|
113
|
+
password: reqEnv('E2E_TEST_PASSWORD'),
|
|
114
|
+
scope: `openid profile ${reqEnv('E2E_SCOPE')}`,
|
|
115
|
+
});
|
|
116
|
+
const url = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
|
|
117
|
+
const res = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
120
|
+
body,
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
const txt = await res.text();
|
|
124
|
+
throw new Error(
|
|
125
|
+
`ROPC token fetch failed (${res.status}). ` +
|
|
126
|
+
`Check: ROPC enabled on app registration, test user exists, ` +
|
|
127
|
+
`MFA excluded via Conditional Access. Response: ${txt}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return res.json() as Promise<{ access_token: string; id_token: string; expires_in: number }>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function reqEnv(k: string): string {
|
|
134
|
+
const v = process.env[k];
|
|
135
|
+
if (!v) throw new Error(`Missing env ${k} — run /infra to regenerate .env.test`);
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function globalSetup() {
|
|
140
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
141
|
+
const token = await fetchRopcToken();
|
|
142
|
+
const baseURL = reqEnv('E2E_BASE_URL');
|
|
143
|
+
const browser = await chromium.launch();
|
|
144
|
+
const page = await browser.newPage();
|
|
145
|
+
await page.goto(baseURL);
|
|
146
|
+
await page.evaluate((t) => {
|
|
147
|
+
localStorage.setItem('e2e.access_token', t.access_token);
|
|
148
|
+
localStorage.setItem('e2e.id_token', t.id_token);
|
|
149
|
+
}, token);
|
|
150
|
+
await page.context().storageState({ path: STATE });
|
|
151
|
+
await browser.close();
|
|
152
|
+
console.log('E2E auth ready — real Entra token injected, MFA UI bypassed.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export default globalSetup;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `docker-compose.test.yml` — materialize to project root
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# docker-compose.test.yml — E2E test stack (local-docker mode).
|
|
162
|
+
# TEMPLATE — adjust build contexts and ports for your project layout.
|
|
163
|
+
services:
|
|
164
|
+
backend:
|
|
165
|
+
build:
|
|
166
|
+
context: ./backend # <-- adjust
|
|
167
|
+
environment:
|
|
168
|
+
SQL_SERVER: ${E2E_SQL_SERVER}
|
|
169
|
+
SQL_DB: ${E2E_SQL_DB}
|
|
170
|
+
SQL_USER: ${E2E_SQL_USER}
|
|
171
|
+
SQL_PASSWORD: ${E2E_SQL_PASSWORD}
|
|
172
|
+
TENANT_ID: ${E2E_TENANT_ID}
|
|
173
|
+
CLIENT_ID: ${E2E_CLIENT_ID}
|
|
174
|
+
STORAGE_CONN: ${E2E_STORAGE_CONN:-}
|
|
175
|
+
ports:
|
|
176
|
+
- "8081:8081"
|
|
177
|
+
healthcheck:
|
|
178
|
+
test: ["CMD", "curl", "-fs", "http://localhost:8081/health"]
|
|
179
|
+
interval: 5s
|
|
180
|
+
timeout: 3s
|
|
181
|
+
retries: 12
|
|
182
|
+
|
|
183
|
+
webapp:
|
|
184
|
+
build:
|
|
185
|
+
context: ./frontend # <-- adjust
|
|
186
|
+
environment:
|
|
187
|
+
API_BASE_URL: http://backend:8081
|
|
188
|
+
TENANT_ID: ${E2E_TENANT_ID}
|
|
189
|
+
CLIENT_ID: ${E2E_CLIENT_ID}
|
|
190
|
+
ports:
|
|
191
|
+
- "8080:80"
|
|
192
|
+
depends_on:
|
|
193
|
+
backend:
|
|
194
|
+
condition: service_healthy
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `scripts/e2e-stack.sh` — materialize to `scripts/`
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
#!/usr/bin/env bash
|
|
201
|
+
# scripts/e2e-stack.sh — E2E test stack lifecycle.
|
|
202
|
+
set -euo pipefail
|
|
203
|
+
cd "$(dirname "$0")/.."
|
|
204
|
+
|
|
205
|
+
CMD="${1:-}"
|
|
206
|
+
COMPOSE="docker-compose.test.yml"
|
|
207
|
+
ENVFILE=".env.test"
|
|
208
|
+
|
|
209
|
+
[ -f "$ENVFILE" ] || { echo "missing $ENVFILE — run /infra to generate it"; exit 1; }
|
|
210
|
+
set -a; . "./$ENVFILE"; set +a
|
|
211
|
+
|
|
212
|
+
if [ "${E2E_STACK_EXTERNAL:-}" = "1" ]; then
|
|
213
|
+
echo "[e2e-stack] E2E_STACK_EXTERNAL=1 — targeting deployed Azure app at ${E2E_BASE_URL:-<unset>}"
|
|
214
|
+
case "$CMD" in
|
|
215
|
+
up)
|
|
216
|
+
echo "[e2e-stack] Verifying Azure app is reachable..."
|
|
217
|
+
for i in $(seq 1 10); do
|
|
218
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}/health" >/dev/null 2>&1 \
|
|
219
|
+
|| curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
220
|
+
echo "[e2e-stack] Azure app reachable."
|
|
221
|
+
exit 0
|
|
222
|
+
fi
|
|
223
|
+
sleep 3
|
|
224
|
+
done
|
|
225
|
+
echo "[e2e-stack] WARNING: Azure app not reachable. Classify as AZURE UNAVAILABLE."
|
|
226
|
+
exit 1
|
|
227
|
+
;;
|
|
228
|
+
down)
|
|
229
|
+
echo "[e2e-stack] External mode — nothing to stop."
|
|
230
|
+
exit 0
|
|
231
|
+
;;
|
|
232
|
+
*)
|
|
233
|
+
echo "usage: e2e-stack.sh up|down"
|
|
234
|
+
exit 1
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if docker compose version >/dev/null 2>&1; then DC="docker compose"; else DC="docker-compose"; fi
|
|
240
|
+
|
|
241
|
+
case "$CMD" in
|
|
242
|
+
up)
|
|
243
|
+
echo "[e2e-stack] building + starting local app containers..."
|
|
244
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" up -d --build
|
|
245
|
+
for i in $(seq 1 30); do
|
|
246
|
+
if curl -fs "${E2E_BASE_URL:-http://localhost:8080}" >/dev/null 2>&1; then
|
|
247
|
+
echo "[e2e-stack] up."
|
|
248
|
+
exit 0
|
|
249
|
+
fi
|
|
250
|
+
sleep 3
|
|
251
|
+
done
|
|
252
|
+
echo "[e2e-stack] ERROR: app did not become healthy — STACK NOT READY"
|
|
253
|
+
exit 1
|
|
254
|
+
;;
|
|
255
|
+
down)
|
|
256
|
+
echo "[e2e-stack] stopping local containers..."
|
|
257
|
+
$DC -f "$COMPOSE" --env-file "$ENVFILE" down
|
|
258
|
+
echo "[e2e-stack] down."
|
|
259
|
+
;;
|
|
260
|
+
*)
|
|
261
|
+
echo "usage: e2e-stack.sh up|down"
|
|
262
|
+
exit 1
|
|
263
|
+
;;
|
|
264
|
+
esac
|
|
265
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spec-conformance.sh — block GREEN until the phase spec is fully filled and every
|
|
3
|
+
# case is verified by a boundary test (v2.2). Runs between RED and GREEN, replacing
|
|
4
|
+
# the v2.1 human TEST REVIEW gate. (§5.55, rule 18)
|
|
5
|
+
#
|
|
6
|
+
# Two checks, both loud-fail (nonzero), no side effects:
|
|
7
|
+
# 1. NO `TBD` remains in docs/spec-phase-<n>.md or any fixture it references.
|
|
8
|
+
# 2. EVERY case-id in the spec table is cited by a test via `# spec: <case-id>`
|
|
9
|
+
# (or `// spec: <case-id>`) under tests/ or e2e/.
|
|
10
|
+
#
|
|
11
|
+
# bash scripts/spec-conformance.sh <phase-n>
|
|
12
|
+
#
|
|
13
|
+
# The case→test citation here, plus the placement convention (contract/transition
|
|
14
|
+
# tests live in tests/regression/), is what closes the rule-14 chain: reachable
|
|
15
|
+
# transition → filled case (rule 14) → cited boundary test (this) → regression corpus.
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
18
|
+
|
|
19
|
+
N="${1:-}"
|
|
20
|
+
[ -n "$N" ] || { echo "spec-conformance: usage: spec-conformance.sh <phase-n>" >&2; exit 2; }
|
|
21
|
+
|
|
22
|
+
SPEC="docs/spec-phase-${N}.md"
|
|
23
|
+
[ -f "$SPEC" ] || { echo "spec-conformance: $SPEC not found." >&2; exit 2; }
|
|
24
|
+
|
|
25
|
+
FAIL=0
|
|
26
|
+
|
|
27
|
+
# ---- 1. no TBD left in the spec ----
|
|
28
|
+
if grep -nE '\bTBD\b' "$SPEC" >/dev/null 2>&1; then
|
|
29
|
+
echo "spec-conformance: FAIL — unfilled 'TBD' marker(s) remain in $SPEC:" >&2
|
|
30
|
+
grep -nE '\bTBD\b' "$SPEC" | sed 's/^/ /' >&2
|
|
31
|
+
echo " Every case must be filled (or escalated as CASE-SET-DIVERGENCE) before GREEN." >&2
|
|
32
|
+
FAIL=1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# ---- referenced fixtures: must exist + carry no TBD ----
|
|
36
|
+
FIXREFS="$(grep -oE 'fixtures/[A-Za-z0-9_./-]+\.json' "$SPEC" | sort -u || true)"
|
|
37
|
+
if [ -n "$FIXREFS" ]; then
|
|
38
|
+
while IFS= read -r fx; do
|
|
39
|
+
[ -n "$fx" ] || continue
|
|
40
|
+
if [ ! -f "$fx" ]; then
|
|
41
|
+
echo "spec-conformance: FAIL — referenced fixture missing: $fx" >&2
|
|
42
|
+
FAIL=1
|
|
43
|
+
elif grep -nE '\bTBD\b' "$fx" >/dev/null 2>&1; then
|
|
44
|
+
echo "spec-conformance: FAIL — 'TBD' marker in fixture: $fx" >&2
|
|
45
|
+
FAIL=1
|
|
46
|
+
fi
|
|
47
|
+
done <<EOF
|
|
48
|
+
$FIXREFS
|
|
49
|
+
EOF
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ---- 2. every case-id cited by a boundary test ----
|
|
53
|
+
# case-ids = the FIRST table-column cell of each row, matching PH<n>-... exactly.
|
|
54
|
+
# (Parse the column, not a free grep — else `fixtures/PH1-ORDER-01-input.json`
|
|
55
|
+
# would yield a bogus `PH1-ORDER-01-input` case-id.)
|
|
56
|
+
CASE_IDS="$(grep -E '^\|' "$SPEC" \
|
|
57
|
+
| awk -F'|' '{ id=$2; gsub(/^[ \t]+|[ \t]+$/,"",id); print id }' \
|
|
58
|
+
| grep -E "^PH${N}-[A-Za-z0-9_-]+$" | sort -u || true)"
|
|
59
|
+
if [ -z "$CASE_IDS" ]; then
|
|
60
|
+
echo "spec-conformance: WARN — no PH${N}-* case-ids found in $SPEC (empty spec?)." >&2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
SEARCH_DIRS=""
|
|
64
|
+
[ -d tests ] && SEARCH_DIRS="$SEARCH_DIRS tests"
|
|
65
|
+
[ -d e2e ] && SEARCH_DIRS="$SEARCH_DIRS e2e"
|
|
66
|
+
|
|
67
|
+
if [ -n "$CASE_IDS" ]; then
|
|
68
|
+
if [ -z "$SEARCH_DIRS" ]; then
|
|
69
|
+
echo "spec-conformance: FAIL — case-ids exist but no tests/ or e2e/ dir to cite them." >&2
|
|
70
|
+
FAIL=1
|
|
71
|
+
else
|
|
72
|
+
while IFS= read -r cid; do
|
|
73
|
+
[ -n "$cid" ] || continue
|
|
74
|
+
# cite convention: `spec: <case-id>` with a non-id char (or EOL) after, so
|
|
75
|
+
# PH2-ORDER-01 does not match PH2-ORDER-011.
|
|
76
|
+
if grep -rEq -- "spec:[[:space:]]*${cid}([^0-9A-Za-z_-]|$)" $SEARCH_DIRS 2>/dev/null; then
|
|
77
|
+
:
|
|
78
|
+
else
|
|
79
|
+
echo "spec-conformance: FAIL — case ${cid} has no boundary test citing it (# spec: ${cid})." >&2
|
|
80
|
+
FAIL=1
|
|
81
|
+
fi
|
|
82
|
+
done <<EOF
|
|
83
|
+
$CASE_IDS
|
|
84
|
+
EOF
|
|
85
|
+
fi
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
if [ "$FAIL" -ne 0 ]; then
|
|
89
|
+
echo "spec-conformance: BLOCKED — fix the above before GREEN." >&2
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
echo "spec-conformance: OK — phase ${N} spec fully filled; every case-id cited by a boundary test."
|
|
93
|
+
exit 0
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# spec-staleness.sh — guard the per-phase spec header against upstream drift (v2.2).
|
|
3
|
+
#
|
|
4
|
+
# The behavioral contract docs/spec-phase-<n>.md carries a GENERATED header that
|
|
5
|
+
# transcludes the phase's PLAN.md `- acceptance:` line + the in-scope DATAFLOW.md
|
|
6
|
+
# transition rows, plus an embedded hash. If PLAN.md / DATAFLOW.md change after the
|
|
7
|
+
# skeleton was stamped (e.g. via /replan), the stored hash no longer matches the
|
|
8
|
+
# upstream → the spec must be regenerated/re-filled before SPEC REVIEW. (§5.55, rule 17)
|
|
9
|
+
#
|
|
10
|
+
# Hashing lives ONLY here (no duplication in agent prompts):
|
|
11
|
+
# bash scripts/spec-staleness.sh stamp <n> # compute + embed the hash (at /overview, on regenerate)
|
|
12
|
+
# bash scripts/spec-staleness.sh check <n> # default: recompute + compare; nonzero on drift
|
|
13
|
+
# bash scripts/spec-staleness.sh <n> # alias for: check <n>
|
|
14
|
+
#
|
|
15
|
+
# set -euo pipefail; nonzero = loud fail; `stamp` is the only side effect.
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
cd "${CLAUDE_PROJECT_DIR:-.}"
|
|
18
|
+
|
|
19
|
+
# ---- args ----
|
|
20
|
+
SUB="check"
|
|
21
|
+
case "${1:-}" in
|
|
22
|
+
stamp) SUB="stamp"; shift;;
|
|
23
|
+
check) SUB="check"; shift;;
|
|
24
|
+
esac
|
|
25
|
+
N="${1:-}"
|
|
26
|
+
[ -n "$N" ] || { echo "spec-staleness: usage: spec-staleness.sh [stamp|check] <phase-n>" >&2; exit 2; }
|
|
27
|
+
|
|
28
|
+
SPEC="docs/spec-phase-${N}.md"
|
|
29
|
+
PLAN="docs/PLAN.md"
|
|
30
|
+
DATAFLOW="docs/DATAFLOW.md"
|
|
31
|
+
[ -f "$SPEC" ] || { echo "spec-staleness: $SPEC not found." >&2; exit 2; }
|
|
32
|
+
[ -f "$PLAN" ] || { echo "spec-staleness: $PLAN not found." >&2; exit 2; }
|
|
33
|
+
|
|
34
|
+
# ---- sha256 of stdin (portable: sha256sum -> shasum -> node) ----
|
|
35
|
+
sha_stdin() {
|
|
36
|
+
if command -v sha256sum >/dev/null 2>&1; then sha256sum | cut -d' ' -f1
|
|
37
|
+
elif command -v shasum >/dev/null 2>&1; then shasum -a 256 | cut -d' ' -f1
|
|
38
|
+
else node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(require('crypto').createHash('sha256').update(d).digest('hex')))"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# normalize: NFC-ish arrow unify (→ -> ->), collapse spaces, trim. Content-sensitive,
|
|
43
|
+
# whitespace-insensitive — so cosmetic reflow does not trip the guard, content does.
|
|
44
|
+
norm() { sed -e 's/→/->/g' -e 's/[[:space:]]\{1,\}/ /g' -e 's/^ //' -e 's/ $//'; }
|
|
45
|
+
|
|
46
|
+
# ---- extract this phase's block from PLAN.md (## Phase <n>: ... up to next ## Phase) ----
|
|
47
|
+
phase_block() {
|
|
48
|
+
awk -v n="$N" '
|
|
49
|
+
$0 ~ ("^## Phase " n ":") {inblk=1; print; next}
|
|
50
|
+
inblk && /^## Phase / {inblk=0}
|
|
51
|
+
inblk {print}
|
|
52
|
+
' "$PLAN"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
BLOCK="$(phase_block || true)"
|
|
56
|
+
[ -n "$BLOCK" ] || { echo "spec-staleness: no '## Phase ${N}:' block in $PLAN." >&2; exit 2; }
|
|
57
|
+
|
|
58
|
+
# acceptance line(s) + dataflow line, normalized
|
|
59
|
+
ACC_LINE="$(printf '%s\n' "$BLOCK" | grep -E '^- acceptance:' | norm || true)"
|
|
60
|
+
DF_LINE="$(printf '%s\n' "$BLOCK" | grep -E '^- dataflow:' | norm || true)"
|
|
61
|
+
|
|
62
|
+
# in-scope transition tokens (from->to) named on the dataflow line
|
|
63
|
+
TOKENS="$(printf '%s\n' "$DF_LINE" | norm | grep -oE '[A-Za-z0-9_]+->[A-Za-z0-9_]+' | sort -u || true)"
|
|
64
|
+
|
|
65
|
+
# matching DATAFLOW.md data-rows (table rows naming an in-scope transition)
|
|
66
|
+
DF_ROWS=""
|
|
67
|
+
if [ -n "$TOKENS" ] && [ -f "$DATAFLOW" ]; then
|
|
68
|
+
while IFS= read -r tok; do
|
|
69
|
+
[ -n "$tok" ] || continue
|
|
70
|
+
rows="$(grep -E '^\|' "$DATAFLOW" | norm | grep -F "$tok" || true)"
|
|
71
|
+
DF_ROWS="${DF_ROWS}${rows}
|
|
72
|
+
"
|
|
73
|
+
done <<EOF
|
|
74
|
+
$TOKENS
|
|
75
|
+
EOF
|
|
76
|
+
fi
|
|
77
|
+
DF_ROWS="$(printf '%s\n' "$DF_ROWS" | grep -v '^$' | sort -u || true)"
|
|
78
|
+
|
|
79
|
+
# ---- the upstream signature + its hash ----
|
|
80
|
+
SIG="$(printf 'ACC:%s\nDF:%s\nROWS:\n%s\n' "$ACC_LINE" "$DF_LINE" "$DF_ROWS")"
|
|
81
|
+
WANT="$(printf '%s' "$SIG" | sha_stdin)"
|
|
82
|
+
|
|
83
|
+
HASH_RE='^<!-- spec-hash: .* -->$'
|
|
84
|
+
HAVE="$(grep -E "$HASH_RE" "$SPEC" | head -1 | sed -E 's/^<!-- spec-hash: (.*) -->$/\1/' || true)"
|
|
85
|
+
|
|
86
|
+
if [ "$SUB" = "stamp" ]; then
|
|
87
|
+
if grep -qE "$HASH_RE" "$SPEC"; then
|
|
88
|
+
tmp="$(mktemp)"
|
|
89
|
+
awk -v h="$WANT" '
|
|
90
|
+
/^<!-- spec-hash: .* -->$/ && !done { print "<!-- spec-hash: " h " -->"; done=1; next }
|
|
91
|
+
{ print }
|
|
92
|
+
' "$SPEC" > "$tmp" && mv "$tmp" "$SPEC"
|
|
93
|
+
else
|
|
94
|
+
echo "spec-staleness: $SPEC has no '<!-- spec-hash: ... -->' header line to stamp." >&2
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
echo "spec-staleness: stamped phase ${N} -> ${WANT}"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# ---- check ----
|
|
102
|
+
if [ -z "$HAVE" ] || [ "$HAVE" = "PENDING" ]; then
|
|
103
|
+
echo "spec-staleness: FAIL — phase ${N} spec hash is unstamped (${HAVE:-missing})." >&2
|
|
104
|
+
echo " Run: bash scripts/spec-staleness.sh stamp ${N}" >&2
|
|
105
|
+
exit 1
|
|
106
|
+
fi
|
|
107
|
+
if [ "$HAVE" != "$WANT" ]; then
|
|
108
|
+
echo "spec-staleness: FAIL — phase ${N} spec is STALE." >&2
|
|
109
|
+
echo " Upstream PLAN.md acceptance / DATAFLOW.md rows changed since the skeleton was stamped." >&2
|
|
110
|
+
echo " stored=${HAVE} current=${WANT}" >&2
|
|
111
|
+
echo " Regenerate the skeleton header from current PLAN/DATAFLOW, re-stamp, and re-fill." >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
echo "spec-staleness: OK — phase ${N} spec header matches upstream (${WANT})."
|
|
115
|
+
exit 0
|