@dzhechkov/p-replicator 1.10.3 → 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.
- package/.dz-manifest.json +119 -47
- package/CHANGELOG.md +85 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +3 -3
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/04_api_reference.md +11 -5
- package/README/eng/README.md +1 -1
- package/README/ru/01_quickstart.md +3 -3
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/04_api_reference.md +11 -5
- package/README/ru/README.md +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +154 -38
- package/bin/cli.js +0 -0
- package/package.json +15 -13
- package/sbom.json +226 -46
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/rule-components.json +11 -0
- package/src/utils.js +3 -8
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/commands/feature.md +57 -9
- package/templates/.claude/commands/go.md +11 -0
- package/templates/.claude/commands/harvest.md +41 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +10 -1
- package/templates/.claude/commands/start.md +8 -0
- package/templates/.claude/hooks/check-ports.cjs +409 -20
- package/templates/.claude/hooks/session-insights.cjs +158 -25
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/hooks/write-insight.cjs +253 -0
- package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
- package/templates/.claude/rules/docker-ports.md +41 -19
- package/templates/.claude/rules/feature-lifecycle.md +14 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/replicate-pipeline.md +4 -2
- package/templates/.claude/rules/skill-interface-protocol.md +1 -0
- package/templates/.claude/rules/swarm-file-evidence.md +46 -0
- package/templates/.claude/settings.json +13 -1
- package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +86 -16
- package/tests/e2e/lifecycle.test.js +55 -9
- package/tests/e2e/packed-insights-writer.test.js +308 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
- package/tests/snapshot/baseline.json +24 -20
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +729 -2
- package/tests/unit/db-port-rule.test.js +36 -5
- package/tests/unit/detection-ladder-contract.test.js +302 -0
- package/tests/unit/detection-ladder-registry.test.js +52 -0
- package/tests/unit/doctor-insight-flow.test.js +315 -0
- package/tests/unit/external-dependency-check.test.js +19 -19
- package/tests/unit/honest-failure-rules.test.js +492 -0
- package/tests/unit/hooks-project-anchored.test.js +67 -3
- package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
- package/tests/unit/insights-dz-delegation.test.js +197 -0
- package/tests/unit/insights-writer.test.js +285 -0
- package/tests/unit/shipped-suite-context.test.js +3 -1
- package/tests/unit/traceability-machine-ids.test.js +413 -0
- package/tests/unit/traceability-negative-fixture.test.js +322 -0
- package/tests/unit/utils.test.js +3 -2
- 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
|
package/src/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
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
|
-
|
|
126
|
-
|
|
203
|
+
const gitVersion = spawnSync('git', ['--version'], { encoding: 'utf8' });
|
|
204
|
+
if (gitVersion.status === 0) {
|
|
127
205
|
pass('git on PATH');
|
|
128
|
-
}
|
|
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
|
-
|
|
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
|
|