@mmerterden/multi-agent-pipeline 12.2.0 → 12.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/CHANGELOG.md +52 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +68 -32
- package/pipeline/lib/fetch-figma-annotations.sh +237 -0
- package/pipeline/multi-agent-refs/analysis-template.md +233 -123
- package/pipeline/schemas/analysis-spec.schema.json +78 -0
- package/pipeline/schemas/figma-project-config.schema.json +49 -0
- package/pipeline/schemas/prefs.schema.json +22 -0
- package/pipeline/scripts/fixtures/install-layout.tsv +4 -4
- package/pipeline/scripts/smoke-fetchers-offline.sh +41 -0
- package/pipeline/scripts/smoke-validate-analysis-doc.sh +161 -0
- package/pipeline/scripts/validate-analysis-doc.mjs +196 -0
|
@@ -49,6 +49,24 @@
|
|
|
49
49
|
"default": "v3",
|
|
50
50
|
"description": "Template version emitted. v3 is the platform-agnostic + Pass B render template (Locked 22)."
|
|
51
51
|
},
|
|
52
|
+
"options": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"description": "Coverage opt-ins captured at Phase 0 Step 5a. Absent = defaults (no UI tests, basic accessibility).",
|
|
56
|
+
"properties": {
|
|
57
|
+
"uiTests": {
|
|
58
|
+
"type": "boolean",
|
|
59
|
+
"default": false,
|
|
60
|
+
"description": "When true, Section 15.6 renders UI test scenarios (XCUITest / Compose UI test)."
|
|
61
|
+
},
|
|
62
|
+
"a11yDepth": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"enum": ["basic", "full"],
|
|
65
|
+
"default": "basic",
|
|
66
|
+
"description": "basic = Section 16.1 checklist only; full = also render the 16.2 VoiceOver/TalkBack walkthrough."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
52
70
|
"evidenceDigest": {
|
|
53
71
|
"type": ["string", "null"],
|
|
54
72
|
"pattern": "^sha256:[0-9a-f]{64}$",
|
|
@@ -110,6 +128,8 @@
|
|
|
110
128
|
"url": { "type": "string", "format": "uri" },
|
|
111
129
|
"fileKey": { "type": ["string", "null"] },
|
|
112
130
|
"nodeId": { "type": ["string", "null"] },
|
|
131
|
+
"screenshotUrl": { "type": ["string", "null"], "description": "Local PNG path or signed render URL for this node." },
|
|
132
|
+
"tier": { "type": ["integer", "null"], "enum": [1, 2, 3, null], "description": "Figma access tier used to capture this record (mirrors state.figmaAccess.tier)." },
|
|
113
133
|
"frames": {
|
|
114
134
|
"type": "array",
|
|
115
135
|
"items": {
|
|
@@ -134,6 +154,64 @@
|
|
|
134
154
|
"repoPath": { "type": ["string", "null"] }
|
|
135
155
|
}
|
|
136
156
|
}
|
|
157
|
+
},
|
|
158
|
+
"codeConnectSnippets": {
|
|
159
|
+
"type": "array",
|
|
160
|
+
"description": "Verbatim CodeConnectSnippet blocks captured on Tier 1 (component name + modifier chain).",
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "object",
|
|
163
|
+
"additionalProperties": false,
|
|
164
|
+
"properties": {
|
|
165
|
+
"nodeId": { "type": ["string", "null"] },
|
|
166
|
+
"component": { "type": "string" },
|
|
167
|
+
"snippet": { "type": ["string", "null"] }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
"tokens": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"items": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"additionalProperties": false,
|
|
176
|
+
"properties": {
|
|
177
|
+
"name": { "type": "string" },
|
|
178
|
+
"value": { "type": ["string", "null"] }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"textLayers": {
|
|
183
|
+
"type": "array",
|
|
184
|
+
"description": "Visible text-layer content. Treated as placeholder copy when an annotation exists for the same node (annotation wins).",
|
|
185
|
+
"items": {
|
|
186
|
+
"type": "object",
|
|
187
|
+
"additionalProperties": false,
|
|
188
|
+
"properties": {
|
|
189
|
+
"nodeId": { "type": ["string", "null"] },
|
|
190
|
+
"name": { "type": ["string", "null"] },
|
|
191
|
+
"text": { "type": "string" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
"annotations": {
|
|
196
|
+
"type": "array",
|
|
197
|
+
"description": "Dev Mode annotations captured as authoritative copy ground truth (Phase 1 annotation ingestion). Empty unless annotations.enabled in the project figma-config.",
|
|
198
|
+
"items": {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"additionalProperties": false,
|
|
201
|
+
"required": ["nodeId", "mode"],
|
|
202
|
+
"properties": {
|
|
203
|
+
"nodeId": { "type": "string" },
|
|
204
|
+
"name": { "type": ["string", "null"] },
|
|
205
|
+
"designText": { "type": ["string", "null"], "description": "The visible-layer text at this node (often a placeholder)." },
|
|
206
|
+
"raw": { "type": ["string", "null"], "description": "The raw annotation label/labelMarkdown before parsing." },
|
|
207
|
+
"parsed": {
|
|
208
|
+
"type": "object",
|
|
209
|
+
"description": "Parsed copy keyed by language code (lowercased), e.g. { tr, en }.",
|
|
210
|
+
"additionalProperties": { "type": "string" }
|
|
211
|
+
},
|
|
212
|
+
"mode": { "type": "string", "enum": ["prefixed", "two-line", "single", "empty"] }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
137
215
|
}
|
|
138
216
|
}
|
|
139
217
|
}
|
|
@@ -50,6 +50,55 @@
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
|
+
"annotations": {
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"description": "Figma Dev Mode annotation ingestion. When enabled, /multi-agent:analysis treats annotations as the authoritative copy for a node (the visible text layer is a placeholder). Absent or disabled = current behavior (visible text layers only).",
|
|
57
|
+
"properties": {
|
|
58
|
+
"enabled": { "type": "boolean", "default": false },
|
|
59
|
+
"langPrefixes": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "type": "string" },
|
|
62
|
+
"default": ["TR", "EN"],
|
|
63
|
+
"description": "Ordered language-code prefixes the annotation parser recognizes, e.g. [\"TR\",\"EN\"]. A `TR: ...\\nEN: ...` label parses by prefix; a two-line label with no prefix maps line 1 -> langPrefixes[0], line 2 -> langPrefixes[1]; a single line -> langPrefixes[0]."
|
|
64
|
+
},
|
|
65
|
+
"devModeCategory": {
|
|
66
|
+
"type": ["string", "null"],
|
|
67
|
+
"description": "Optional Dev Mode annotation category to filter on (e.g. a copy/UX-writing category). Null = accept all annotations on the node."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"localization": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"additionalProperties": false,
|
|
74
|
+
"description": "How this project owns localization. Drives Section 10 of /multi-agent:analysis. Default `in-repo` keeps the historical hand-filled per-locale grid; `externally-owned` defers per-locale values to an authoring system (the analysis names key + status + copy source + base value only).",
|
|
75
|
+
"properties": {
|
|
76
|
+
"ownership": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["in-repo", "externally-owned"],
|
|
79
|
+
"default": "in-repo"
|
|
80
|
+
},
|
|
81
|
+
"authoringPipeline": {
|
|
82
|
+
"type": ["string", "null"],
|
|
83
|
+
"description": "Human-readable reference to the owning localization system when ownership = externally-owned (e.g. a workflow or resource-bot name). Rendered as the Ownership column value; never a secret."
|
|
84
|
+
},
|
|
85
|
+
"locales": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "type": "string" },
|
|
88
|
+
"description": "Supported locale codes for this project (e.g. [\"tr\",\"en\",\"ar\",\"de\",\"es\",\"fr\",\"it\",\"ru\"]). Overrides the built-in default locale set used by Section 10.",
|
|
89
|
+
"minItems": 1
|
|
90
|
+
},
|
|
91
|
+
"baseLanguage": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"default": "en",
|
|
94
|
+
"description": "Source/base language code. A key missing its base-language value is a defect (renders the raw key at runtime)."
|
|
95
|
+
},
|
|
96
|
+
"keyPattern": {
|
|
97
|
+
"type": ["string", "null"],
|
|
98
|
+
"description": "Key naming convention hint, e.g. `Namespace.Case`. Documentation only; not enforced."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
53
102
|
"github": {
|
|
54
103
|
"type": "object",
|
|
55
104
|
"additionalProperties": false,
|
|
@@ -1258,6 +1258,28 @@
|
|
|
1258
1258
|
"figmaConfigPath": {
|
|
1259
1259
|
"type": "string",
|
|
1260
1260
|
"description": "Path to per-project figma-config.json (for Figma pipeline projects)."
|
|
1261
|
+
},
|
|
1262
|
+
"standardsFile": {
|
|
1263
|
+
"type": "string",
|
|
1264
|
+
"description": "Filename (or absolute/tilde path) of the canonical home-dir Standards reference for this project, used by /multi-agent:analysis Q5 auto-detect (e.g. ~/<project>-Standards.md)."
|
|
1265
|
+
},
|
|
1266
|
+
"editableRelatedRepos": {
|
|
1267
|
+
"type": "array",
|
|
1268
|
+
"items": { "type": "string" },
|
|
1269
|
+
"maxItems": 20,
|
|
1270
|
+
"description": "Extra writable repos to offer in the dev-context picker beyond auto-detected submodules (iOS/Android/Backend)."
|
|
1271
|
+
},
|
|
1272
|
+
"frontendRepos": {
|
|
1273
|
+
"type": "array",
|
|
1274
|
+
"items": { "type": "string" },
|
|
1275
|
+
"maxItems": 20,
|
|
1276
|
+
"description": "Frontend repos for this project, used by /multi-agent:analysis Step 4 (Frontend is rarely in the iOS/Android submodule tree)."
|
|
1277
|
+
},
|
|
1278
|
+
"frontendRoots": {
|
|
1279
|
+
"type": "array",
|
|
1280
|
+
"items": { "type": "string" },
|
|
1281
|
+
"maxItems": 20,
|
|
1282
|
+
"description": "Whitelist source roots for the Frontend repo-evidence scan (overrides the default src/ app/ components/ features/ lib/)."
|
|
1261
1283
|
}
|
|
1262
1284
|
}
|
|
1263
1285
|
}
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
.claude/CLAUDE.md 1
|
|
3
3
|
.claude/agents 8
|
|
4
4
|
.claude/commands 44
|
|
5
|
-
.claude/lib
|
|
5
|
+
.claude/lib 27
|
|
6
6
|
.claude/multi-agent-preferences.json 1
|
|
7
7
|
.claude/multi-agent-refs 51
|
|
8
8
|
.claude/rules 12
|
|
9
9
|
.claude/schemas 23
|
|
10
|
-
.claude/scripts
|
|
10
|
+
.claude/scripts 195
|
|
11
11
|
.claude/settings.json 1
|
|
12
12
|
.claude/skills 428
|
|
13
13
|
.copilot/.pipeline-version 1
|
|
14
14
|
.copilot/agents 8
|
|
15
15
|
.copilot/copilot-instructions.md 1
|
|
16
|
-
.copilot/lib
|
|
16
|
+
.copilot/lib 27
|
|
17
17
|
.copilot/schemas 23
|
|
18
|
-
.copilot/scripts
|
|
18
|
+
.copilot/scripts 195
|
|
19
19
|
.copilot/skills 467
|
|
@@ -354,6 +354,46 @@ assert_in_file "^-i" "$WORK/security-argv.log" "security invoked in -i stdin mod
|
|
|
354
354
|
assert_not_in_file "$SECRET_VALUE" "$WORK/security-argv.log" "secret absent from security argv"
|
|
355
355
|
assert_in_file "$SECRET_VALUE" "$WORK/security-stdin.log" "secret delivered via stdin"
|
|
356
356
|
|
|
357
|
+
# -----------------------------------------------------------------------------
|
|
358
|
+
note "7b. fetch-figma-annotations.sh (Tier-2 annotation ingestion)"
|
|
359
|
+
# -----------------------------------------------------------------------------
|
|
360
|
+
expect_rc 1 "no args exits 1 (usage)" \
|
|
361
|
+
"$SH" "$LIB/fetch-figma-annotations.sh"
|
|
362
|
+
|
|
363
|
+
# Missing token -> blocked JSON + exit 2 (no network).
|
|
364
|
+
expect_rc 2 "missing token exits 2 with blocked JSON" \
|
|
365
|
+
env HOME="$FAKE_HOME" USER=smoke \
|
|
366
|
+
"$SH" "$LIB/fetch-figma-annotations.sh" --file-key ABC --node-id "1:23"
|
|
367
|
+
assert_in_file "missing-token" "$OUT" "blocked JSON names missing-token"
|
|
368
|
+
|
|
369
|
+
# Fake curl returns a /nodes doc carrying a TR:/EN: annotation; with a stub
|
|
370
|
+
# token the parser must reach + parse it (regression guard for the parser).
|
|
371
|
+
BIN_CURL_ANNOT="$WORK/bin-curl-annot"
|
|
372
|
+
mkdir -p "$BIN_CURL_ANNOT"
|
|
373
|
+
cat > "$BIN_CURL_ANNOT/curl" <<'FAKE'
|
|
374
|
+
#!/bin/bash
|
|
375
|
+
out=""; prev=""
|
|
376
|
+
for a in "$@"; do
|
|
377
|
+
[ "$prev" = "-o" ] && out="$a"
|
|
378
|
+
prev="$a"
|
|
379
|
+
done
|
|
380
|
+
if [ -n "$out" ]; then
|
|
381
|
+
cat > "$out" <<'JSON'
|
|
382
|
+
{"nodes":{"1:23":{"document":{"id":"1:23","name":"Title","type":"TEXT","characters":"Giriniz","annotations":[{"label":"TR: Ucusunuzu secin\nEN: Select your flight"}]}}}}
|
|
383
|
+
JSON
|
|
384
|
+
fi
|
|
385
|
+
printf '200'
|
|
386
|
+
exit 0
|
|
387
|
+
FAKE
|
|
388
|
+
chmod +x "$BIN_CURL_ANNOT/curl"
|
|
389
|
+
|
|
390
|
+
expect_rc 0 "stub token + annotation doc parses (exit 0)" \
|
|
391
|
+
env HOME="$FAKE_HOME" USER=smoke FIGMA_PAT=fake-token \
|
|
392
|
+
PATH="$BIN_CURL_ANNOT:$PATH" \
|
|
393
|
+
"$SH" "$LIB/fetch-figma-annotations.sh" --file-key ABC --node-id "1:23" --lang-prefixes TR,EN
|
|
394
|
+
assert_in_file "\"mode\": \"prefixed\"" "$OUT" "annotation parsed as prefixed"
|
|
395
|
+
assert_in_file "Select your flight" "$OUT" "EN copy extracted from annotation"
|
|
396
|
+
|
|
357
397
|
# -----------------------------------------------------------------------------
|
|
358
398
|
note "8. static source checks (bash-3.2 compat + no secrets on argv)"
|
|
359
399
|
# -----------------------------------------------------------------------------
|
|
@@ -363,6 +403,7 @@ OWNED_FILES=(
|
|
|
363
403
|
"$LIB/fetch-swagger.sh"
|
|
364
404
|
"$LIB/fetch-fortify.sh"
|
|
365
405
|
"$LIB/fetch-graylog.sh"
|
|
406
|
+
"$LIB/fetch-figma-annotations.sh"
|
|
366
407
|
"$LIB/figma-mcp-refresh.sh"
|
|
367
408
|
"$LIB/figma-screenshot.sh"
|
|
368
409
|
"$LIB/credential-store.sh"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-validate-analysis-doc.sh - contract for validate-analysis-doc.mjs,
|
|
3
|
+
# the deterministic gate over an emitted /multi-agent:analysis document.
|
|
4
|
+
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
V="$SCRIPT_DIR/validate-analysis-doc.mjs"
|
|
9
|
+
|
|
10
|
+
PASS=0
|
|
11
|
+
FAIL=0
|
|
12
|
+
record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
|
|
13
|
+
record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
|
|
14
|
+
|
|
15
|
+
printf '\342\206\222 smoke-validate-analysis-doc: emitted-doc gate\n'
|
|
16
|
+
|
|
17
|
+
tmp=$(mktemp -d "${TMPDIR:-/tmp}/vdoc.XXXXXX")
|
|
18
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
19
|
+
|
|
20
|
+
# A well-formed Full-mode doc (neutral UserProfile domain, BR referenced downstream).
|
|
21
|
+
cat > "$tmp/good.md" <<'MD'
|
|
22
|
+
---
|
|
23
|
+
feature: UserProfile
|
|
24
|
+
platform: ios
|
|
25
|
+
language: tr
|
|
26
|
+
mode: full
|
|
27
|
+
template_version: v3
|
|
28
|
+
---
|
|
29
|
+
## 1. Özet
|
|
30
|
+
Profile screen.
|
|
31
|
+
## 2. Hedefler ve Karşı Hedefler
|
|
32
|
+
goals.
|
|
33
|
+
## 4. Kullanıcı Hikayeleri
|
|
34
|
+
### 4.4 İş Kuralları
|
|
35
|
+
| BR-profile-01 | rule | source | Given/When/Then |
|
|
36
|
+
## 9. API Kontratları
|
|
37
|
+
api.
|
|
38
|
+
## 13. Mimari Plan
|
|
39
|
+
arch.
|
|
40
|
+
## 14. Eklenecek Dosyalar
|
|
41
|
+
files.
|
|
42
|
+
## 15. Test Planı
|
|
43
|
+
BR-profile-01 happy path.
|
|
44
|
+
## 20. Riskler ve Açık Sorular
|
|
45
|
+
risk.
|
|
46
|
+
## 21. Referanslar
|
|
47
|
+
refs.
|
|
48
|
+
MD
|
|
49
|
+
|
|
50
|
+
if node "$V" "$tmp/good.md" >/dev/null 2>&1; then
|
|
51
|
+
record_pass "well-formed Full doc passes (exit 0)"
|
|
52
|
+
else
|
|
53
|
+
record_fail "well-formed doc should pass"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Lite doc without Section 15 must still pass (Locked 31 mode-aware).
|
|
57
|
+
cat > "$tmp/lite.md" <<'MD'
|
|
58
|
+
---
|
|
59
|
+
feature: UserProfile
|
|
60
|
+
platform: ios
|
|
61
|
+
language: en
|
|
62
|
+
mode: lite
|
|
63
|
+
template_version: v3
|
|
64
|
+
---
|
|
65
|
+
## 1. Summary
|
|
66
|
+
s
|
|
67
|
+
## 2. Goals and Non-Goals
|
|
68
|
+
g
|
|
69
|
+
## 4. User Stories
|
|
70
|
+
### 4.4 Business Rules
|
|
71
|
+
| BR-profile-01 | rule | source | Given/When/Then |
|
|
72
|
+
## 9. API Contracts
|
|
73
|
+
a
|
|
74
|
+
## 13. Architecture
|
|
75
|
+
x
|
|
76
|
+
## 14. Files to Add
|
|
77
|
+
f
|
|
78
|
+
## 20. Risks and Open Questions
|
|
79
|
+
r
|
|
80
|
+
## 21. References
|
|
81
|
+
ref
|
|
82
|
+
MD
|
|
83
|
+
if node "$V" "$tmp/lite.md" >/dev/null 2>&1; then
|
|
84
|
+
record_pass "Lite doc (no Section 15) passes - mode-aware Locked 31"
|
|
85
|
+
else
|
|
86
|
+
record_fail "Lite doc without Section 15 must pass"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Missing front-matter -> ERROR.
|
|
90
|
+
printf '## 1. Summary\nno front matter\n' > "$tmp/nofm.md"
|
|
91
|
+
node "$V" "$tmp/nofm.md" >/dev/null 2>&1
|
|
92
|
+
[ "$?" -eq 1 ] && record_pass "missing front-matter fails (exit 1)" || record_fail "missing front-matter should fail"
|
|
93
|
+
|
|
94
|
+
# Banned punctuation (em-dash) -> ERROR.
|
|
95
|
+
cp "$tmp/good.md" "$tmp/emdash.md"
|
|
96
|
+
printf 'A line with an em-dash \342\200\224 here.\n' >> "$tmp/emdash.md"
|
|
97
|
+
# Capture first (pipefail + node's nonzero exit would poison a direct pipe).
|
|
98
|
+
emdash_out=$(node "$V" "$tmp/emdash.md" 2>&1 || true)
|
|
99
|
+
if printf '%s' "$emdash_out" | grep -q "em-dash"; then
|
|
100
|
+
record_pass "em-dash flagged (Locked 7)"
|
|
101
|
+
else
|
|
102
|
+
record_fail "em-dash should be flagged"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Missing a never-omitted section (References) -> ERROR.
|
|
106
|
+
grep -v "Referanslar\|References" "$tmp/good.md" > "$tmp/noref.md"
|
|
107
|
+
node "$V" "$tmp/noref.md" >/dev/null 2>&1
|
|
108
|
+
[ "$?" -eq 1 ] && record_pass "missing References section fails" || record_fail "missing References should fail"
|
|
109
|
+
|
|
110
|
+
# Define-only BR under --strict -> fail (WARN promoted).
|
|
111
|
+
cat > "$tmp/orphan.md" <<'MD'
|
|
112
|
+
---
|
|
113
|
+
feature: UserProfile
|
|
114
|
+
platform: ios
|
|
115
|
+
language: en
|
|
116
|
+
mode: full
|
|
117
|
+
template_version: v3
|
|
118
|
+
---
|
|
119
|
+
## 1. Summary
|
|
120
|
+
s
|
|
121
|
+
## 2. Goals
|
|
122
|
+
g
|
|
123
|
+
## 4. User Stories
|
|
124
|
+
### 4.4 Business Rules
|
|
125
|
+
| BR-profile-42 | rule | source | Given/When/Then |
|
|
126
|
+
## 9. API Contracts
|
|
127
|
+
a
|
|
128
|
+
## 13. Architecture
|
|
129
|
+
x
|
|
130
|
+
## 14. Files to Add
|
|
131
|
+
f
|
|
132
|
+
## 15. Test Plan
|
|
133
|
+
no BR reference here
|
|
134
|
+
## 20. Risks
|
|
135
|
+
r
|
|
136
|
+
## 21. References
|
|
137
|
+
ref
|
|
138
|
+
MD
|
|
139
|
+
node "$V" "$tmp/orphan.md" >/dev/null 2>&1
|
|
140
|
+
[ "$?" -eq 0 ] && record_pass "define-only BR is WARN, not error (default)" || record_fail "orphan BR should not hard-fail by default"
|
|
141
|
+
node "$V" "$tmp/orphan.md" --strict >/dev/null 2>&1
|
|
142
|
+
[ "$?" -eq 1 ] && record_pass "define-only BR fails under --strict" || record_fail "orphan BR should fail under --strict"
|
|
143
|
+
|
|
144
|
+
# ui_tests: true but no Section 15.6 -> ERROR.
|
|
145
|
+
sed 's/^mode: full/mode: full\nui_tests: true\na11y_depth: basic/' "$tmp/good.md" > "$tmp/uimiss.md"
|
|
146
|
+
node "$V" "$tmp/uimiss.md" >/dev/null 2>&1
|
|
147
|
+
[ "$?" -eq 1 ] && record_pass "ui_tests:true without Section 15.6 fails" || record_fail "ui_tests:true must require 15.6"
|
|
148
|
+
|
|
149
|
+
# ui_tests: true WITH a Section 15.6 heading -> passes.
|
|
150
|
+
cp "$tmp/uimiss.md" "$tmp/uiok.md"
|
|
151
|
+
printf '## 15.6 UI test senaryolari\nUI-profile-01 flow.\n' >> "$tmp/uiok.md"
|
|
152
|
+
node "$V" "$tmp/uiok.md" >/dev/null 2>&1
|
|
153
|
+
[ "$?" -eq 0 ] && record_pass "ui_tests:true with Section 15.6 passes" || record_fail "ui_tests:true + 15.6 should pass"
|
|
154
|
+
|
|
155
|
+
# a11y_depth: full but no Section 16.2 walkthrough -> ERROR.
|
|
156
|
+
sed 's/^mode: full/mode: full\nui_tests: false\na11y_depth: full/' "$tmp/good.md" > "$tmp/a11ymiss.md"
|
|
157
|
+
node "$V" "$tmp/a11ymiss.md" >/dev/null 2>&1
|
|
158
|
+
[ "$?" -eq 1 ] && record_pass "a11y_depth:full without Section 16.2 fails" || record_fail "a11y_depth:full must require 16.2"
|
|
159
|
+
|
|
160
|
+
printf '\n\342\225\220\342\225\220 smoke-validate-analysis-doc: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
|
|
161
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// validate-analysis-doc.mjs - deterministic validator for an EMITTED
|
|
3
|
+
// /multi-agent:analysis document (analysis/<feature>-<platform>.md).
|
|
4
|
+
//
|
|
5
|
+
// The analysis SKILL states many "fails the dispatch gate" invariants as prose
|
|
6
|
+
// the model is asked to self-enforce. This turns the high-value, mechanically
|
|
7
|
+
// checkable ones into a real gate so a malformed spec is caught before it is
|
|
8
|
+
// handed to a human or to /multi-agent:dev.
|
|
9
|
+
//
|
|
10
|
+
// Zero deps. Reads one markdown file (path arg or STDIN).
|
|
11
|
+
//
|
|
12
|
+
// Checks (ERROR = blocking exit 1; WARN = advisory, still exit 0 unless --strict):
|
|
13
|
+
// - Front-matter block with required keys (feature, platform, language, mode,
|
|
14
|
+
// template_version); mode in {full, lite}; platform in the known set.
|
|
15
|
+
// - Never-omitted sections present by title keyword (Locked 2): Summary,
|
|
16
|
+
// Goals, User Stories, API Contracts, Architecture, Files to Add, Risks,
|
|
17
|
+
// References.
|
|
18
|
+
// - Humanizer punctuation policy (Locked 7): no em-dash / en-dash / ellipsis /
|
|
19
|
+
// section-sign / curly quotes anywhere in the body.
|
|
20
|
+
// - Traceability (Full mode only, Locked 31): every BR-<slug>-NN id defined in
|
|
21
|
+
// the Business Rules area is referenced again later (test / acceptance /
|
|
22
|
+
// analytics). A define-only rule is a WARN (heuristic).
|
|
23
|
+
// - No whole-section "TBD" / "Not applicable" placeholder bodies (Locked 2);
|
|
24
|
+
// the inline "[label TBD - see Open Questions]" marker is allowed.
|
|
25
|
+
//
|
|
26
|
+
// Usage:
|
|
27
|
+
// node validate-analysis-doc.mjs analysis/UserProfile-ios.md
|
|
28
|
+
// cat doc.md | node validate-analysis-doc.mjs -
|
|
29
|
+
// node validate-analysis-doc.mjs doc.md --strict # WARN also fails
|
|
30
|
+
//
|
|
31
|
+
// Exit: 0 valid, 1 invalid (or WARN under --strict), 64 usage error.
|
|
32
|
+
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
|
|
35
|
+
const KNOWN_PLATFORMS = new Set(["ios", "android", "backend", "frontend"]);
|
|
36
|
+
const REQUIRED_FM = ["feature", "platform", "language", "mode", "template_version"];
|
|
37
|
+
|
|
38
|
+
// Never-omitted sections (Locked 2), matched by bilingual title keyword so the
|
|
39
|
+
// re-flowed section numbering does not matter.
|
|
40
|
+
const REQUIRED_SECTIONS = [
|
|
41
|
+
{ key: "summary", any: ["Summary", "Özet", "Ozet"] },
|
|
42
|
+
{ key: "goals", any: ["Goals", "Hedefler"] },
|
|
43
|
+
{ key: "user stories", any: ["User Stories", "Kullanıcı Hikayeleri", "Kullanici Hikayeleri"] },
|
|
44
|
+
{ key: "api contracts", any: ["API Contracts", "API Kontratları", "API Kontratlari"] },
|
|
45
|
+
{ key: "architecture", any: ["Architecture", "Mimari"] },
|
|
46
|
+
{ key: "files to add", any: ["Files to Add", "Eklenecek Dosyalar"] },
|
|
47
|
+
{ key: "risks", any: ["Risks", "Riskler"] },
|
|
48
|
+
{ key: "references", any: ["References", "Referanslar"] },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Humanizer punctuation policy (Locked 7) - banned codepoints.
|
|
52
|
+
const BANNED_PUNCT = [
|
|
53
|
+
{ ch: "—", name: "em-dash" },
|
|
54
|
+
{ ch: "–", name: "en-dash" },
|
|
55
|
+
{ ch: "…", name: "ellipsis" },
|
|
56
|
+
{ ch: "§", name: "section-sign" },
|
|
57
|
+
{ ch: "“", name: "curly-double-open" },
|
|
58
|
+
{ ch: "”", name: "curly-double-close" },
|
|
59
|
+
{ ch: "‘", name: "curly-single-open" },
|
|
60
|
+
{ ch: "’", name: "curly-single-close" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function readInput() {
|
|
64
|
+
const args = process.argv.slice(2).filter((a) => a !== "--strict");
|
|
65
|
+
const arg = args[0];
|
|
66
|
+
if (!arg) {
|
|
67
|
+
console.error("usage: validate-analysis-doc.mjs <path|-> [--strict]");
|
|
68
|
+
process.exit(64);
|
|
69
|
+
}
|
|
70
|
+
if (arg === "-") return readFileSync(0, "utf-8");
|
|
71
|
+
return readFileSync(arg, "utf-8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseFrontMatter(text) {
|
|
75
|
+
const lines = text.split("\n");
|
|
76
|
+
if (lines[0].trim() !== "---") return null;
|
|
77
|
+
const end = lines.indexOf("---", 1);
|
|
78
|
+
if (end < 0) return null;
|
|
79
|
+
const fm = {};
|
|
80
|
+
for (let i = 1; i < end; i++) {
|
|
81
|
+
const m = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/);
|
|
82
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
83
|
+
}
|
|
84
|
+
return { fm, bodyStart: end + 1 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main() {
|
|
88
|
+
const text = readInput();
|
|
89
|
+
const strict = process.argv.includes("--strict");
|
|
90
|
+
const errors = [];
|
|
91
|
+
const warns = [];
|
|
92
|
+
|
|
93
|
+
// 1. Front-matter
|
|
94
|
+
const parsed = parseFrontMatter(text);
|
|
95
|
+
if (!parsed) {
|
|
96
|
+
errors.push("missing YAML front-matter block (--- ... ---) at the top");
|
|
97
|
+
} else {
|
|
98
|
+
for (const k of REQUIRED_FM) {
|
|
99
|
+
if (!parsed.fm[k]) errors.push(`front-matter missing required key: ${k}`);
|
|
100
|
+
}
|
|
101
|
+
const mode = parsed.fm.mode;
|
|
102
|
+
if (mode && mode !== "full" && mode !== "lite") {
|
|
103
|
+
errors.push(`front-matter mode must be full|lite, got: ${mode}`);
|
|
104
|
+
}
|
|
105
|
+
const plat = parsed.fm.platform;
|
|
106
|
+
if (plat && !KNOWN_PLATFORMS.has(plat)) {
|
|
107
|
+
errors.push(`front-matter platform not in known set: ${plat}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const mode = parsed?.fm?.mode || "full";
|
|
111
|
+
|
|
112
|
+
// 2. Never-omitted sections (by heading keyword)
|
|
113
|
+
const headings = text
|
|
114
|
+
.split("\n")
|
|
115
|
+
.filter((l) => /^#{1,3}\s/.test(l))
|
|
116
|
+
.map((l) => l.replace(/^#{1,3}\s/, "").trim());
|
|
117
|
+
const headingBlob = headings.join("\n");
|
|
118
|
+
for (const sec of REQUIRED_SECTIONS) {
|
|
119
|
+
if (!sec.any.some((kw) => headingBlob.includes(kw))) {
|
|
120
|
+
errors.push(`missing required section (Locked 2): ${sec.key}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2b. Opt-in coverage sections must be present when the front-matter says so.
|
|
125
|
+
const uiTests = String(parsed?.fm?.ui_tests || "false").toLowerCase() === "true";
|
|
126
|
+
const a11yDepth = String(parsed?.fm?.a11y_depth || "basic").toLowerCase();
|
|
127
|
+
const headingBlobLower = headingBlob.toLowerCase();
|
|
128
|
+
if (uiTests) {
|
|
129
|
+
const hasUi =
|
|
130
|
+
headingBlob.includes("15.6") ||
|
|
131
|
+
headingBlobLower.includes("ui test") ||
|
|
132
|
+
headingBlobLower.includes("ui-test");
|
|
133
|
+
if (!hasUi) {
|
|
134
|
+
errors.push("front-matter ui_tests: true but no Section 15.6 UI test scenarios found (Locked 31)");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (a11yDepth === "full") {
|
|
138
|
+
const hasWalkthrough =
|
|
139
|
+
headingBlob.includes("16.2") ||
|
|
140
|
+
headingBlobLower.includes("walkthrough") ||
|
|
141
|
+
headingBlobLower.includes("gezinme");
|
|
142
|
+
if (!hasWalkthrough) {
|
|
143
|
+
errors.push("front-matter a11y_depth: full but no Section 16.2 screen-reader walkthrough found");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3. Humanizer punctuation
|
|
148
|
+
const bodyLines = text.split("\n");
|
|
149
|
+
for (let i = 0; i < bodyLines.length; i++) {
|
|
150
|
+
for (const b of BANNED_PUNCT) {
|
|
151
|
+
if (bodyLines[i].includes(b.ch)) {
|
|
152
|
+
errors.push(`banned punctuation ${b.name} at line ${i + 1} (Locked 7 humanizer policy)`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4. Placeholder whole-section bodies (Locked 2)
|
|
158
|
+
if (/^\s*(TBD|Not applicable|N\/A)\s*$/im.test(text)) {
|
|
159
|
+
warns.push('a line is a bare "TBD"/"Not applicable"/"N/A" body; omit the section instead (Locked 2)');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 5. Traceability (Full mode only, Locked 31)
|
|
163
|
+
if (mode === "full") {
|
|
164
|
+
const idRe = /BR-[a-z0-9]+(?:-[a-z0-9]+)*-\d+/gi;
|
|
165
|
+
const all = text.match(idRe) || [];
|
|
166
|
+
const counts = new Map();
|
|
167
|
+
for (const id of all) {
|
|
168
|
+
const key = id.toUpperCase();
|
|
169
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
if (counts.size === 0) {
|
|
172
|
+
warns.push("Full mode but no BR-<slug>-NN business-rule ids found (Section 4.4 / Locked 31)");
|
|
173
|
+
}
|
|
174
|
+
for (const [id, n] of counts) {
|
|
175
|
+
if (n < 2) {
|
|
176
|
+
warns.push(`business rule ${id} is defined but never referenced downstream (test/acceptance/analytics) - Locked 31 traceability`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Report
|
|
182
|
+
for (const w of warns) console.error(`WARN: ${w}`);
|
|
183
|
+
for (const e of errors) console.error(`ERROR: ${e}`);
|
|
184
|
+
if (errors.length > 0) {
|
|
185
|
+
console.error(`validate-analysis-doc: FAIL (${errors.length} error(s), ${warns.length} warning(s))`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (strict && warns.length > 0) {
|
|
189
|
+
console.error(`validate-analysis-doc: FAIL under --strict (${warns.length} warning(s))`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
console.log(`validate-analysis-doc: OK (mode=${mode}, ${warns.length} warning(s))`);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main();
|