@dzhechkov/p-replicator 1.10.4 → 1.12.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 (68) hide show
  1. package/.dz-manifest.json +119 -47
  2. package/CHANGELOG.md +85 -0
  3. package/MULTIPLATFORM_ROADMAP.md +1 -1
  4. package/README/eng/01_quickstart.md +3 -3
  5. package/README/eng/02_user_guide.md +1 -1
  6. package/README/eng/03_admin_guide.md +2 -2
  7. package/README/eng/04_api_reference.md +11 -5
  8. package/README/eng/README.md +1 -1
  9. package/README/ru/01_quickstart.md +3 -3
  10. package/README/ru/02_user_guide.md +1 -1
  11. package/README/ru/03_admin_guide.md +2 -2
  12. package/README/ru/04_api_reference.md +11 -5
  13. package/README/ru/README.md +1 -1
  14. package/README/ru/html/index.html +7 -7
  15. package/README.md +154 -38
  16. package/bin/cli.js +0 -0
  17. package/package.json +12 -10
  18. package/sbom.json +226 -46
  19. package/scripts/check-pipeline-gaps.sh +413 -0
  20. package/src/commands/doctor.js +94 -4
  21. package/src/rule-components.json +11 -0
  22. package/src/utils.js +3 -8
  23. package/templates/.claude/agents/harvest-coordinator.md +10 -1
  24. package/templates/.claude/commands/feature.md +57 -9
  25. package/templates/.claude/commands/go.md +11 -0
  26. package/templates/.claude/commands/harvest.md +41 -3
  27. package/templates/.claude/commands/myinsights.md +21 -26
  28. package/templates/.claude/commands/replicate.md +10 -1
  29. package/templates/.claude/commands/start.md +8 -0
  30. package/templates/.claude/hooks/check-ports.cjs +409 -20
  31. package/templates/.claude/hooks/session-insights.cjs +158 -25
  32. package/templates/.claude/hooks/statusline.cjs +2 -2
  33. package/templates/.claude/hooks/write-insight.cjs +253 -0
  34. package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
  35. package/templates/.claude/rules/docker-ports.md +41 -19
  36. package/templates/.claude/rules/feature-lifecycle.md +14 -3
  37. package/templates/.claude/rules/honest-configuration.md +54 -0
  38. package/templates/.claude/rules/insights-capture.md +10 -5
  39. package/templates/.claude/rules/replicate-pipeline.md +4 -2
  40. package/templates/.claude/rules/skill-interface-protocol.md +1 -0
  41. package/templates/.claude/rules/swarm-file-evidence.md +46 -0
  42. package/templates/.claude/settings.json +13 -1
  43. package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
  44. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +86 -16
  45. package/tests/e2e/lifecycle.test.js +55 -9
  46. package/tests/e2e/packed-insights-writer.test.js +308 -0
  47. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
  48. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
  49. package/tests/snapshot/baseline.json +24 -20
  50. package/tests/snapshot/templates.test.js +47 -0
  51. package/tests/unit/absence-is-not-emptiness.test.js +15 -1
  52. package/tests/unit/check-pipeline-gaps.test.js +94 -0
  53. package/tests/unit/check-ports.test.js +729 -2
  54. package/tests/unit/db-port-rule.test.js +36 -5
  55. package/tests/unit/detection-ladder-contract.test.js +302 -0
  56. package/tests/unit/detection-ladder-registry.test.js +52 -0
  57. package/tests/unit/doctor-insight-flow.test.js +315 -0
  58. package/tests/unit/external-dependency-check.test.js +19 -19
  59. package/tests/unit/honest-failure-rules.test.js +492 -0
  60. package/tests/unit/hooks-project-anchored.test.js +67 -3
  61. package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
  62. package/tests/unit/insights-dz-delegation.test.js +197 -0
  63. package/tests/unit/insights-writer.test.js +285 -0
  64. package/tests/unit/shipped-suite-context.test.js +3 -1
  65. package/tests/unit/traceability-machine-ids.test.js +413 -0
  66. package/tests/unit/traceability-negative-fixture.test.js +322 -0
  67. package/tests/unit/utils.test.js +3 -2
  68. package/LICENSE +0 -21
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env bash
2
+ # Package-owned deterministic traceability gate for role-mapped SPARC documents.
3
+ set -uo pipefail
4
+
5
+ export LC_ALL=C
6
+
7
+ usage() {
8
+ cat >&2 <<'USAGE'
9
+ Usage: check-pipeline-gaps.sh PROJECT_ROOT [--traceability]
10
+ [--role-map-source PATH] [--project-role-map-source PATH]
11
+
12
+ Exit 0: all checked ID sets are equal
13
+ Exit 1: a named content or traceability gap was established
14
+ Exit 2: a trustworthy comparison could not be established
15
+ USAGE
16
+ }
17
+
18
+ not_established() {
19
+ printf 'NOT-ESTABLISHED %s\n' "$*"
20
+ INCONCLUSIVE_COUNT=$((INCONCLUSIVE_COUNT + 1))
21
+ }
22
+
23
+ content_gap() {
24
+ printf '%s\n' "$*"
25
+ GAP_COUNT=$((GAP_COUNT + 1))
26
+ }
27
+
28
+ validate_role_target() {
29
+ local role="$1" value="$2" source="$3"
30
+ if [ -z "$value" ]; then
31
+ printf 'NOT-ESTABLISHED role-map=%s role=%s empty target\n' "$source" "$role"
32
+ return 1
33
+ fi
34
+ case "$value" in
35
+ /*|../*|*/../*|*/..|..)
36
+ printf 'NOT-ESTABLISHED role-map=%s role=%s escaping target=%s\n' \
37
+ "$source" "$role" "$value"
38
+ return 1
39
+ ;;
40
+ esac
41
+ return 0
42
+ }
43
+
44
+ parse_role_map() {
45
+ local source="$1" heading="$2" prefix="$3"
46
+ local section line role value variable seen_spec=0 seen_pseudo=0 seen_arch=0 seen_ref=0 seen_comp=0
47
+
48
+ if [ ! -f "$source" ] || [ ! -r "$source" ] || [ -L "$source" ]; then
49
+ printf 'NOT-ESTABLISHED role-map=%s is missing unreadable or a symlink\n' "$source"
50
+ return 1
51
+ fi
52
+
53
+ section="$(awk -v wanted="$heading" '
54
+ BEGIN { in_section = 0; in_yaml = 0; in_map = 0 }
55
+ { sub(/\r$/, "") }
56
+ $0 == wanted { in_section = 1; next }
57
+ in_section && /^#{1,3}[[:space:]]/ { exit }
58
+ in_section && $0 == "```yaml" { in_yaml = 1; next }
59
+ in_yaml && $0 == "DOCUMENT_ROLE_MAP:" { in_map = 1; next }
60
+ in_map && /^```/ { exit }
61
+ in_map { print }
62
+ ' "$source")"
63
+ if [ -z "$section" ]; then
64
+ printf 'NOT-ESTABLISHED role-map=%s heading=%s has no DOCUMENT_ROLE_MAP\n' "$source" "$heading"
65
+ return 1
66
+ fi
67
+
68
+ while IFS= read -r line; do
69
+ [ -z "$line" ] && continue
70
+ if [[ ! "$line" =~ ^[[:space:]][[:space:]]([a-zA-Z0-9_-]+):[[:space:]]*(.*)$ ]]; then
71
+ printf 'NOT-ESTABLISHED role-map=%s malformed line=%s\n' "$source" "$line"
72
+ return 1
73
+ fi
74
+ role="${BASH_REMATCH[1]}"
75
+ value="${BASH_REMATCH[2]}"
76
+ value="${value%$'\r'}"
77
+ while [[ "$value" == *[[:space:]] ]]; do value="${value%?}"; done
78
+ case "$role" in
79
+ specification) variable=SPECIFICATION; seen_spec=$((seen_spec + 1)) ;;
80
+ pseudocode) variable=PSEUDOCODE; seen_pseudo=$((seen_pseudo + 1)) ;;
81
+ architecture) variable=ARCHITECTURE; seen_arch=$((seen_arch + 1)) ;;
82
+ refinement) variable=REFINEMENT; seen_ref=$((seen_ref + 1)) ;;
83
+ completion) variable=COMPLETION; seen_comp=$((seen_comp + 1)) ;;
84
+ *) printf 'NOT-ESTABLISHED role-map=%s unknown role=%s\n' "$source" "$role"; return 1 ;;
85
+ esac
86
+ if ! validate_role_target "$role" "$value" "$source"; then return 1; fi
87
+ printf -v "${prefix}_${variable}" '%s' "$value"
88
+ done <<< "$section"
89
+
90
+ if [ "$seen_spec" -ne 1 ] || [ "$seen_pseudo" -ne 1 ] || [ "$seen_arch" -ne 1 ] || \
91
+ [ "$seen_ref" -ne 1 ] || [ "$seen_comp" -ne 1 ]; then
92
+ printf 'NOT-ESTABLISHED role-map=%s requires exactly specification pseudocode architecture refinement completion; counts=%s,%s,%s,%s,%s\n' \
93
+ "$source" "$seen_spec" "$seen_pseudo" "$seen_arch" "$seen_ref" "$seen_comp"
94
+ return 1
95
+ fi
96
+ }
97
+
98
+ extract_specification() {
99
+ local source="$1" output="$2"
100
+ awk '
101
+ BEGIN { in_fence = 0; fence = "" }
102
+ {
103
+ sub(/\r$/, "")
104
+ trimmed = $0
105
+ sub(/^[[:space:]]*/, "", trimmed)
106
+ prefix = substr(trimmed, 1, 3)
107
+ if (prefix == "```" || prefix == "~~~") {
108
+ marker = substr(prefix, 1, 1)
109
+ if (!in_fence) { in_fence = 1; fence = marker }
110
+ else if (marker == fence) { in_fence = 0; fence = "" }
111
+ next
112
+ }
113
+ if (in_fence) next
114
+ }
115
+ /^###[[:space:]]+(FR|NFR|AC)-/ {
116
+ line = $0
117
+ sub(/^###[[:space:]]+/, "", line)
118
+ if (match(line, /^(FR|NFR|AC)-[A-Za-z0-9]+(-[A-Za-z0-9]+)*-[0-9]+([[:space:]]|$)/)) {
119
+ id = substr(line, RSTART, RLENGTH)
120
+ sub(/[[:space:]]$/, "", id)
121
+ print "ID\t" id
122
+ } else {
123
+ print "MALFORMED\t" NR "\t" line
124
+ }
125
+ }
126
+ ' "$source" > "$output"
127
+ }
128
+
129
+ extract_pseudocode() {
130
+ local source="$1" output="$2"
131
+ awk '
132
+ BEGIN { in_algorithm = 0; claims = 0; algorithm_line = 0; in_fence = 0; fence = "" }
133
+ {
134
+ sub(/\r$/, "")
135
+ trimmed = $0
136
+ sub(/^[[:space:]]*/, "", trimmed)
137
+ prefix = substr(trimmed, 1, 3)
138
+ if (prefix == "```" || prefix == "~~~") {
139
+ marker = substr(prefix, 1, 1)
140
+ if (!in_fence) { in_fence = 1; fence = marker }
141
+ else if (marker == fence) { in_fence = 0; fence = "" }
142
+ next
143
+ }
144
+ if (in_fence) next
145
+ }
146
+ /^###[[:space:]]+Algorithm:/ {
147
+ if (in_algorithm && claims == 0) print "UNKEYED\t" algorithm_line
148
+ in_algorithm = 1
149
+ claims = 0
150
+ algorithm_line = NR
151
+ next
152
+ }
153
+ /^#/ {
154
+ if (in_algorithm && claims == 0) print "UNKEYED\t" algorithm_line
155
+ in_algorithm = 0
156
+ claims = 0
157
+ next
158
+ }
159
+ in_algorithm && /^REQUIREMENT:/ {
160
+ line = $0
161
+ if (match(line, /^REQUIREMENT: `(FR|NFR|AC)-[A-Za-z0-9]+(-[A-Za-z0-9]+)*-[0-9]+`$/)) {
162
+ sub(/^REQUIREMENT: `/, "", line)
163
+ sub(/`$/, "", line)
164
+ print "ID\t" line
165
+ claims++
166
+ } else {
167
+ print "MALFORMED\t" NR "\t" line
168
+ }
169
+ }
170
+ END { if (in_algorithm && claims == 0) print "UNKEYED\t" algorithm_line }
171
+ ' "$source" > "$output"
172
+ }
173
+
174
+ validate_extraction() {
175
+ local records="$1" role="$2" contour="$3" expected_slug="$4" ids="$5"
176
+ local kind first rest id duplicate
177
+ : > "$ids"
178
+ while IFS=$'\t' read -r kind first rest; do
179
+ [ -z "$kind" ] && continue
180
+ case "$kind" in
181
+ ID)
182
+ id="$first"
183
+ if [ -n "$expected_slug" ] && [[ ! "$id" =~ ^(FR|NFR|AC)-${expected_slug}-[0-9]+$ ]]; then
184
+ content_gap "MALFORMED $contour $role expected-slug=$expected_slug id=$id"
185
+ else
186
+ printf '%s\n' "$id" >> "$ids"
187
+ fi
188
+ ;;
189
+ MALFORMED)
190
+ content_gap "MALFORMED $contour $role line=$first value=$rest"
191
+ ;;
192
+ UNKEYED)
193
+ content_gap "GAP $contour $role unkeyed-algorithm line=$first"
194
+ ;;
195
+ *)
196
+ content_gap "MALFORMED $contour $role extractor-record=$kind"
197
+ ;;
198
+ esac
199
+ done < "$records"
200
+
201
+ if [ -s "$ids" ]; then
202
+ while IFS= read -r duplicate; do
203
+ [ -n "$duplicate" ] && content_gap "DUPLICATE $contour $role $duplicate"
204
+ done < <(sort "$ids" | uniq -d)
205
+ fi
206
+ }
207
+
208
+ safe_role_file() {
209
+ local contour="$1" role="$2" directory="$3" target="$4"
210
+ local file="$directory/$target" current="$directory" component
211
+ local -a target_components
212
+ IFS='/' read -r -a target_components <<< "$target"
213
+ for component in "${target_components[@]}"; do
214
+ current="$current/$component"
215
+ if [ -L "$current" ]; then
216
+ not_established "contour=$contour role=$role path=$current symlink is not allowed"
217
+ return 1
218
+ fi
219
+ done
220
+ if [ ! -f "$file" ] || [ ! -r "$file" ]; then
221
+ not_established "contour=$contour role=$role path=$file missing or unreadable"
222
+ return 1
223
+ fi
224
+ return 0
225
+ }
226
+
227
+ process_contour() {
228
+ local contour="$1" directory="$2" spec_target="$3" pseudo_target="$4" expected_slug="$5"
229
+ local spec_file="$directory/$spec_target" pseudo_file="$directory/$pseudo_target"
230
+ local stem spec_records pseudo_records spec_raw pseudo_raw spec_ids pseudo_ids missing orphan
231
+ local requirements algorithms missing_count orphan_count id before_gaps
232
+
233
+ printf 'TRACE contour=%s specification=%s pseudocode=%s\n' "$contour" "$spec_file" "$pseudo_file"
234
+ if [ -L "$directory" ]; then
235
+ not_established "contour=$contour path=$directory symlink directory is not allowed"
236
+ return
237
+ fi
238
+ if ! safe_role_file "$contour" specification "$directory" "$spec_target"; then return; fi
239
+ if ! safe_role_file "$contour" pseudocode "$directory" "$pseudo_target"; then return; fi
240
+
241
+ CONTOUR_SEQ=$((CONTOUR_SEQ + 1))
242
+ stem="$TEMP_DIR/contour-$CONTOUR_SEQ"
243
+ spec_records="$stem.spec.records"
244
+ pseudo_records="$stem.pseudo.records"
245
+ spec_raw="$stem.spec.raw"
246
+ pseudo_raw="$stem.pseudo.raw"
247
+ spec_ids="$stem.spec.ids"
248
+ pseudo_ids="$stem.pseudo.ids"
249
+ missing="$stem.missing.ids"
250
+ orphan="$stem.orphan.ids"
251
+
252
+ if ! extract_specification "$spec_file" "$spec_records"; then
253
+ not_established "contour=$contour specification extractor failed path=$spec_file"
254
+ return
255
+ fi
256
+ if ! extract_pseudocode "$pseudo_file" "$pseudo_records"; then
257
+ not_established "contour=$contour pseudocode extractor failed path=$pseudo_file"
258
+ return
259
+ fi
260
+
261
+ before_gaps=$GAP_COUNT
262
+ validate_extraction "$spec_records" specification "$contour" "$expected_slug" "$spec_raw"
263
+ validate_extraction "$pseudo_records" pseudocode "$contour" "$expected_slug" "$pseudo_raw"
264
+ sort -u "$spec_raw" > "$spec_ids"
265
+ sort -u "$pseudo_raw" > "$pseudo_ids"
266
+
267
+ requirements=$(wc -l < "$spec_ids"); requirements=${requirements//[[:space:]]/}
268
+ algorithms=$(wc -l < "$pseudo_ids"); algorithms=${algorithms//[[:space:]]/}
269
+ if [ "$requirements" -eq 0 ] && [ "$algorithms" -eq 0 ]; then
270
+ content_gap "GAP $contour specification no-declared-machine-ids"
271
+ fi
272
+
273
+ # These are deliberately two independent calls. Neither direction short-circuits the other.
274
+ if ! comm -23 "$spec_ids" "$pseudo_ids" > "$missing"; then
275
+ not_established "contour=$contour specification->pseudocode comparison failed"
276
+ return
277
+ fi
278
+ if ! comm -23 "$pseudo_ids" "$spec_ids" > "$orphan"; then
279
+ not_established "contour=$contour pseudocode->specification comparison failed"
280
+ return
281
+ fi
282
+
283
+ missing_count=$(wc -l < "$missing"); missing_count=${missing_count//[[:space:]]/}
284
+ orphan_count=$(wc -l < "$orphan"); orphan_count=${orphan_count//[[:space:]]/}
285
+ printf 'COUNT requirements=%s algorithms=%s missing-algorithm=%s orphan-algorithm=%s\n' \
286
+ "$requirements" "$algorithms" "$missing_count" "$orphan_count"
287
+ while IFS= read -r id; do
288
+ [ -n "$id" ] && content_gap "GAP $contour specification->pseudocode $id"
289
+ done < "$missing"
290
+ while IFS= read -r id; do
291
+ [ -n "$id" ] && content_gap "GAP $contour pseudocode->specification $id"
292
+ done < "$orphan"
293
+
294
+ if [ "$GAP_COUNT" -eq "$before_gaps" ]; then
295
+ printf 'PASS contour=%s bidirectional traceability complete\n' "$contour"
296
+ fi
297
+ }
298
+
299
+ GAP_COUNT=0
300
+ INCONCLUSIVE_COUNT=0
301
+ FEATURE_COUNT=0
302
+ CONTOUR_SEQ=0
303
+
304
+ PROJECT_ROOT="${1:-}"
305
+ if [ -z "$PROJECT_ROOT" ]; then
306
+ usage
307
+ exit 2
308
+ fi
309
+ shift
310
+
311
+ FEATURE_ROLE_MAP_SOURCE=""
312
+ PROJECT_ROLE_MAP_SOURCE=""
313
+ while [ "$#" -gt 0 ]; do
314
+ case "$1" in
315
+ --traceability) shift ;;
316
+ --role-map-source)
317
+ [ "$#" -ge 2 ] || { usage; exit 2; }
318
+ FEATURE_ROLE_MAP_SOURCE="$2"; shift 2
319
+ ;;
320
+ --project-role-map-source)
321
+ [ "$#" -ge 2 ] || { usage; exit 2; }
322
+ PROJECT_ROLE_MAP_SOURCE="$2"; shift 2
323
+ ;;
324
+ *) printf 'NOT-ESTABLISHED unknown argument=%s\n' "$1"; usage; exit 2 ;;
325
+ esac
326
+ done
327
+
328
+ if [ ! -d "$PROJECT_ROOT" ] || [ -L "$PROJECT_ROOT" ]; then
329
+ printf 'NOT-ESTABLISHED project-root=%s missing or a symlink\n' "$PROJECT_ROOT"
330
+ exit 2
331
+ fi
332
+
333
+ for utility in awk sort uniq comm wc mktemp; do
334
+ if ! command -v "$utility" >/dev/null 2>&1; then
335
+ printf 'NOT-ESTABLISHED required utility=%s is unavailable\n' "$utility"
336
+ exit 2
337
+ fi
338
+ done
339
+
340
+ FEATURE_ROLE_MAP_SOURCE="${FEATURE_ROLE_MAP_SOURCE:-$PROJECT_ROOT/.claude/commands/feature.md}"
341
+ PROJECT_ROLE_MAP_SOURCE="${PROJECT_ROLE_MAP_SOURCE:-$PROJECT_ROOT/.claude/skills/sparc-prd-mini/SKILL.md}"
342
+
343
+ if ! parse_role_map "$FEATURE_ROLE_MAP_SOURCE" '### Phase 1 document role map' FEATURE; then
344
+ INCONCLUSIVE_COUNT=$((INCONCLUSIVE_COUNT + 1))
345
+ fi
346
+ if ! parse_role_map "$PROJECT_ROLE_MAP_SOURCE" '### Project-level default' PROJECT; then
347
+ INCONCLUSIVE_COUNT=$((INCONCLUSIVE_COUNT + 1))
348
+ fi
349
+ if [ "$INCONCLUSIVE_COUNT" -gt 0 ]; then
350
+ printf 'VERDICT traceability=NOT-ESTABLISHED features=0 gaps=0 inconclusive=%s\n' \
351
+ "$INCONCLUSIVE_COUNT"
352
+ exit 2
353
+ fi
354
+
355
+ TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/p-replicator-traceability.XXXXXX")" || {
356
+ printf 'NOT-ESTABLISHED could not create private temporary directory\n'
357
+ exit 2
358
+ }
359
+ case "$TEMP_DIR" in
360
+ "${TMPDIR:-/tmp}"/p-replicator-traceability.*) ;;
361
+ *) printf 'NOT-ESTABLISHED unsafe temporary directory=%s\n' "$TEMP_DIR"; exit 2 ;;
362
+ esac
363
+ trap 'if [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ]; then rm -r -- "$TEMP_DIR"; fi' EXIT HUP INT TERM
364
+
365
+ DOCS_ROOT="$PROJECT_ROOT/docs"
366
+ CHECKED_CONTOURS=0
367
+ if [ ! -d "$DOCS_ROOT" ] || [ -L "$DOCS_ROOT" ]; then
368
+ not_established "docs-root=$DOCS_ROOT missing or a symlink"
369
+ else
370
+ PROJECT_SPEC="$DOCS_ROOT/$PROJECT_SPECIFICATION"
371
+ PROJECT_PSEUDO="$DOCS_ROOT/$PROJECT_PSEUDOCODE"
372
+ if [ -e "$PROJECT_SPEC" ] || [ -L "$PROJECT_SPEC" ] || \
373
+ [ -e "$PROJECT_PSEUDO" ] || [ -L "$PROJECT_PSEUDO" ]; then
374
+ process_contour project "$DOCS_ROOT" "$PROJECT_SPECIFICATION" "$PROJECT_PSEUDOCODE" ''
375
+ CHECKED_CONTOURS=$((CHECKED_CONTOURS + 1))
376
+ fi
377
+
378
+ FEATURES_ROOT="$DOCS_ROOT/features"
379
+ if [ -L "$FEATURES_ROOT" ]; then
380
+ not_established "features-root=$FEATURES_ROOT symlink is not allowed"
381
+ elif [ -d "$FEATURES_ROOT" ]; then
382
+ for feature_dir in "$FEATURES_ROOT"/*; do
383
+ if [ ! -e "$feature_dir" ] && [ ! -L "$feature_dir" ]; then continue; fi
384
+ if [ ! -d "$feature_dir" ] && [ ! -L "$feature_dir" ]; then continue; fi
385
+ feature_slug="${feature_dir##*/}"
386
+ FEATURE_COUNT=$((FEATURE_COUNT + 1))
387
+ CHECKED_CONTOURS=$((CHECKED_CONTOURS + 1))
388
+ if [[ ! "$feature_slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
389
+ not_established "contour=$feature_slug invalid feature slug"
390
+ continue
391
+ fi
392
+ process_contour "$feature_slug" "$feature_dir" \
393
+ "$FEATURE_SPECIFICATION" "$FEATURE_PSEUDOCODE" "$feature_slug"
394
+ done
395
+ fi
396
+ fi
397
+
398
+ if [ "$CHECKED_CONTOURS" -eq 0 ]; then
399
+ not_established "project=$PROJECT_ROOT has no applicable project or feature contour"
400
+ fi
401
+
402
+ if [ "$GAP_COUNT" -gt 0 ]; then
403
+ printf 'VERDICT traceability=FAIL features=%s gaps=%s inconclusive=%s\n' \
404
+ "$FEATURE_COUNT" "$GAP_COUNT" "$INCONCLUSIVE_COUNT"
405
+ exit 1
406
+ fi
407
+ if [ "$INCONCLUSIVE_COUNT" -gt 0 ]; then
408
+ printf 'VERDICT traceability=NOT-ESTABLISHED features=%s gaps=0 inconclusive=%s\n' \
409
+ "$FEATURE_COUNT" "$INCONCLUSIVE_COUNT"
410
+ exit 2
411
+ fi
412
+ printf 'VERDICT traceability=PASS features=%s gaps=0 inconclusive=0\n' "$FEATURE_COUNT"
413
+ exit 0
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { execFileSync } = require('child_process');
5
+ const { spawnSync } = require('child_process');
6
6
  const {
7
7
  green, red, yellow, cyan, bold, dim,
8
8
  info, success, warn, error: logError,
@@ -19,6 +19,84 @@ const EXPECTED_COMMANDS = Object.keys(COMPONENTS.commands.items);
19
19
  const EXPECTED_AGENTS = Object.keys(COMPONENTS.agents.items);
20
20
  const EXPECTED_RULES = Object.keys(COMPONENTS.rules.items);
21
21
 
22
+ const FIX_COMMIT_SUBJECT = /^fix(?:\([^\r\n()]+\))?:/;
23
+ const INSIGHT_RECORD_HEADING = /^##\s+(\d{4}-\d{2}-\d{2})\s+—(?:\s|$)/gm;
24
+
25
+ function localDate(date) {
26
+ const year = String(date.getFullYear()).padStart(4, '0');
27
+ const month = String(date.getMonth() + 1).padStart(2, '0');
28
+ const day = String(date.getDate()).padStart(2, '0');
29
+ return `${year}-${month}-${day}`;
30
+ }
31
+
32
+ function insightFlowWindow(referenceTime) {
33
+ const end = new Date(referenceTime.getFullYear(), referenceTime.getMonth(), referenceTime.getDate());
34
+ const start = new Date(end);
35
+ start.setDate(start.getDate() - 2);
36
+ return { startDate: localDate(start), endDate: localDate(end) };
37
+ }
38
+
39
+ function notPerformed(window, reason) {
40
+ return { state: 'not-performed', ...window, reason };
41
+ }
42
+
43
+ function measureInsightFlow(targetDir, referenceTime) {
44
+ const window = insightFlowWindow(referenceTime);
45
+ const gitOptions = {
46
+ cwd: targetDir,
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ timeout: 5000,
50
+ maxBuffer: 1024 * 1024,
51
+ };
52
+
53
+ const repository = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], gitOptions);
54
+ if (repository.status !== 0) {
55
+ if (repository.error && repository.error.code === 'ENOENT') {
56
+ return notPerformed(window, 'git unavailable');
57
+ }
58
+ const gitMarker = path.join(targetDir, '.git');
59
+ return notPerformed(window,
60
+ fs.existsSync(gitMarker) ? 'no readable git history' : 'not a git repository');
61
+ }
62
+ if (repository.stdout.trim() !== 'true') return notPerformed(window, 'not a git repository');
63
+
64
+ const head = spawnSync('git', ['rev-parse', '--verify', 'HEAD'], gitOptions);
65
+ if (head.status !== 0) {
66
+ // Unreadable history is unknown, never a measured zero; mutation-gate protects this branch.
67
+ return {
68
+ state: 'not-performed',
69
+ startDate: window.startDate,
70
+ endDate: window.endDate,
71
+ reason: 'no readable git history',
72
+ };
73
+ }
74
+
75
+ const history = spawnSync('git', [
76
+ 'log', '--format=%s',
77
+ `--since=${window.startDate} 00:00:00`,
78
+ `--until=${window.endDate} 23:59:59`,
79
+ ], gitOptions);
80
+ if (history.status !== 0) {
81
+ return notPerformed(window, 'git history query failed');
82
+ }
83
+ const fixCommits = history.stdout.split(/\r?\n/).filter((subject) =>
84
+ FIX_COMMIT_SUBJECT.test(subject)).length;
85
+
86
+ const insightsIndex = path.join(targetDir, '.claude', 'insights', 'index.md');
87
+ let insights;
88
+ try {
89
+ insights = fs.readFileSync(insightsIndex, 'utf8');
90
+ } catch (err) {
91
+ if (err && err.code === 'ENOENT') insights = '';
92
+ else return notPerformed(window, 'insight carrier is unreadable');
93
+ }
94
+ const insightRecords = Array.from(insights.matchAll(INSIGHT_RECORD_HEADING))
95
+ .filter((match) => match[1] >= window.startDate && match[1] <= window.endDate).length;
96
+
97
+ return { state: 'measured', ...window, fixCommits, insightRecords };
98
+ }
99
+
22
100
  function run(options) {
23
101
  const { targetDir } = options;
24
102
 
@@ -122,10 +200,10 @@ function run(options) {
122
200
 
123
201
  // ── 6d) Prerequisites (system tools the package relies on) ──────────────
124
202
  console.log(bold('Prerequisites:'));
125
- try {
126
- execFileSync('git', ['--version'], { stdio: 'pipe' });
203
+ const gitVersion = spawnSync('git', ['--version'], { encoding: 'utf8' });
204
+ if (gitVersion.status === 0) {
127
205
  pass('git on PATH');
128
- } catch {
206
+ } else {
129
207
  fail('git NOT on PATH — autocommit hooks (roadmap, insights, plans) will silently no-op');
130
208
  }
131
209
  console.log('');
@@ -160,6 +238,18 @@ function run(options) {
160
238
  }
161
239
  console.log('');
162
240
 
241
+ const referenceTime = options.now === undefined ? new Date() : new Date(options.now);
242
+ const insightFlow = measureInsightFlow(targetDir, referenceTime);
243
+ const flowPeriod = `${insightFlow.startDate} through ${insightFlow.endDate}`;
244
+ // This evidence is advisory: plain output keeps it outside doctor health accounting.
245
+ if (insightFlow.state === 'measured') {
246
+ console.log(`Insight flow (${flowPeriod}): ${insightFlow.fixCommits} fix commits and `
247
+ + `${insightFlow.insightRecords} insight records`);
248
+ } else {
249
+ console.log(`Insight flow (${flowPeriod}): check NOT performed — ${insightFlow.reason}`);
250
+ }
251
+ console.log('');
252
+
163
253
  // ── 8) Summary ──────────────────────────────────────────────────────────
164
254
  console.log(bold('\u2500'.repeat(50)));
165
255
  if (issues === 0 && warnings === 0) {
@@ -0,0 +1,11 @@
1
+ {
2
+ "cost-of-detection-ladder": "Five enforcement layers, signal-fit check selection, and required reactions",
3
+ "replicate-pipeline": "Phase sequence, output paths, git discipline, modular skill loading",
4
+ "skill-interface-protocol": "view() contract, module interface, maturity tagging, composition rules",
5
+ "git-workflow": "Commit/push discipline, branch strategy, semantic messages",
6
+ "insights-capture": "When and how to capture development insights to knowledge base",
7
+ "feature-lifecycle": "/feature phases (PLAN → VALIDATE → IMPLEMENT → REVIEW), checkpoints, scoring",
8
+ "docker-ports": "Правило №0: storage ports are not published outward; loopback bind is the exception",
9
+ "swarm-file-evidence": "Parallel work delivers named terminal file receipts; narrative and silence are not results",
10
+ "honest-configuration": "Required external values refuse absent, degenerate, unknown, or unreachable meanings"
11
+ }
package/src/utils.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const RULE_COMPONENTS = require('./rule-components.json');
5
6
 
6
7
  // ===========================================================================
7
8
  // Colors — ANSI escape codes (zero dependencies)
@@ -326,14 +327,7 @@ const COMPONENTS = {
326
327
  kind: 'pre-shipped',
327
328
  label: 'Rules (pipeline + workflow constraints)',
328
329
  group: 'core',
329
- items: {
330
- 'replicate-pipeline': 'Phase sequence, output paths, git discipline, modular skill loading',
331
- 'skill-interface-protocol': 'view() contract, module interface, maturity tagging, composition rules',
332
- 'git-workflow': 'Commit/push discipline, branch strategy, semantic messages',
333
- 'insights-capture': 'When and how to capture development insights to knowledge base',
334
- 'feature-lifecycle': '/feature phases (PLAN → VALIDATE → IMPLEMENT → REVIEW), checkpoints, scoring',
335
- 'docker-ports': 'Правило №0: storage ports are not published outward; loopback bind is the exception',
336
- },
330
+ items: RULE_COMPONENTS,
337
331
  },
338
332
  settings: {
339
333
  src: '.claude/settings.json',
@@ -357,6 +351,7 @@ const COMPONENTS = {
357
351
  'autocommit-plans': 'Auto-commit docs/plans/ on Stop hook',
358
352
  'statusline': 'Multi-line dashboard (pipeline, roadmap, toolkit) for Claude Code statusLine',
359
353
  'state-update': 'Argv-driven helper for pipeline commands to publish current command + phase + progress',
354
+ 'write-insight': 'Validate and append one harvest insight to the project Markdown carrier',
360
355
  'check-ports': 'Enforce docker-ports Правило №0 against a real compose (invoke deliberately; exits 0/1/2)',
361
356
  'check-growth-trace': 'Did the M5 growth seed reach docs/Specification.md (invoke deliberately; exits 0/1/2)',
362
357
  'check-docs-complete': 'Are Phase-1 documents written and placeholder-free, before the Phase-2 swarm (invoke deliberately; exits 0/1/2)',
@@ -37,8 +37,17 @@ Launch 5 parallel agents via Task tool:
37
37
  - Project root path
38
38
  - `TOOLKIT_HARVEST.md` content (if exists)
39
39
  - Instructions from its module section in `modules/01-agent-review.md`
40
+ - A unique `WORK_UNIT_ID` and absolute `TRACE_PATH` under one coordinator-owned `RUN_ID`
40
41
 
41
- **Merge results:** Deduplicate, cross-reference, sort by reusability confidence.
42
+ Each agent must write a substantive body ending in `Status: completed` or `Status: failed` to its
43
+ unique `TRACE_PATH` before returning a one-line pointer. Before merge, verify every path is a regular
44
+ non-symlink file, non-whitespace, post-launch, and terminal. Narrative output or silence is not a
45
+ receipt. Name missing, stale, partial, unreadable, duplicate, failed, dead-PID, or probe-error agents
46
+ and refuse synthesis/completion unless every required receipt is valid and completed. See
47
+ `.claude/rules/swarm-file-evidence.md`.
48
+
49
+ **Merge results:** Read only validated completed traces, then deduplicate, cross-reference, and sort
50
+ by reusability confidence.
42
51
 
43
52
  ## Quality Gate (Phase 3)
44
53