@dzhechkov/p-replicator 1.10.4 → 1.13.1
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 +312 -76
- package/CHANGELOG.md +232 -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/05_architecture.md +1 -1
- package/README/eng/README.md +2 -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/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +9 -9
- package/README.md +278 -39
- package/package.json +5 -4
- package/sbom.json +665 -75
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +15 -0
- package/src/utils.js +35 -11
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +81 -9
- package/templates/.claude/commands/go.md +9 -0
- package/templates/.claude/commands/harvest.md +39 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +171 -37
- package/templates/.claude/commands/start.md +29 -0
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +434 -24
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- 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/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +13 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +123 -52
- 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/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +234 -716
- 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 +72 -46
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +773 -2
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +43 -6
- 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/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +574 -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/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -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 +40 -2
|
@@ -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) {
|
package/src/commands/init.js
CHANGED
|
@@ -190,7 +190,7 @@ function run(options) {
|
|
|
190
190
|
console.log(bold('Next steps:'));
|
|
191
191
|
console.log(` 1. Open ${cyan('Claude Code')} in this directory`);
|
|
192
192
|
console.log(` 2. Run ${cyan('/replicate "Your product idea"')} for the full pipeline`);
|
|
193
|
-
console.log(` 3. Follow
|
|
193
|
+
console.log(` 3. Follow 5-6 phases with interactive checkpoints (Phase 0 optional, Phase 0.5 always)`);
|
|
194
194
|
console.log('');
|
|
195
195
|
console.log(bold('Available commands after /replicate:'));
|
|
196
196
|
console.log(` ${cyan('/replicate')} ${dim('\u2014 Full pipeline: idea \u2192 validated docs \u2192 toolkit')}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
"embeddable-widget": "A widget runs on someone else's page: cross-origin requests, style isolation, host CSP — and the check belongs on a FOREIGN origin",
|
|
12
|
+
"incoming-webhooks": "Событие приходит дважды и от кого угодно: названный ключ повторности с атомарным исключением, подпись до разбора сырого тела, порядок доставки не гарантирован",
|
|
13
|
+
"long-running-job": "Work that takes minutes needs a handle issued before it starts, three distinguishable states, and a retry that continues instead of restarting",
|
|
14
|
+
"model-call-cost": "External model calls are billed per call and triggered by strangers: a ceiling is a NUMBER, reaching it REFUSES, and an unconfigured ceiling fails the boot"
|
|
15
|
+
}
|
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,9 +351,24 @@ 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)',
|
|
357
|
+
'check-look-trace': 'Did the Phase-0.5 source-look seed reach docs/Specification.md (invoke deliberately; exits 0/1/2)',
|
|
358
|
+
'check-look-origin': 'Did a third-party-analysis HYPOTHESIS row reach Specification.md without dated live confirmation (invoke deliberately; exits 0/1/2)',
|
|
359
|
+
'capture-source-path': 'Phase-0.5 «путь» axis: click through the source product in a browser and emit FR-LOOK rows (external Playwright; exits 0/1/2)',
|
|
362
360
|
'check-docs-complete': 'Are Phase-1 documents written and placeholder-free, before the Phase-2 swarm (invoke deliberately; exits 0/1/2)',
|
|
361
|
+
'check-swarm-receipts': 'Did every parallel work unit deliver its named terminal file, before the coordinator aggregates (invoke deliberately; exits 0/1/2)',
|
|
362
|
+
'check-embed-contract': 'Was the embeddable widget checked on a FOREIGN origin — cross-origin, style leakage, host CSP (invoke deliberately; exits 0/1/2)',
|
|
363
|
+
'check-webhook-contract': 'Is the incoming webhook signed, deduplicated by a NAMED repeat key with atomic exclusion, and safe against reordering (invoke deliberately; exits 0/1/2)',
|
|
364
|
+
'check-job-contract': 'Does long-running work carry a handle issued before the work, three distinguishable states and a retry that continues (invoke deliberately; exits 0/1/2)',
|
|
365
|
+
'check-model-cost': 'Does every external model call name a ceiling that can BIND — a number, per user and per day, refusing at the limit (invoke deliberately; exits 0/1/2)',
|
|
366
|
+
'check-canon': 'Before a WRITING fan-out: is the shared canon named, pinned by sha256 and free of colliding ordinals and self-contradicting lists (invoke deliberately; exits 0/1/2)',
|
|
367
|
+
'check-file-ownership': 'Before a WRITING fan-out: does every written file have exactly one writer — coordinator included — and does a file born by a SPLIT get its owner at creation (invoke deliberately; exits 0/1/2)',
|
|
368
|
+
'check-source-version': 'Before a merge: does every edit and every verdict declare the sha256 of the source it was built on, and does it still match (invoke deliberately; exits 0/1/2)',
|
|
369
|
+
'check-handoff-manifest': 'Phase 0 to Phase 1: did every enumerated discovery output get an answer — cited by id, or rejected with a reason (invoke deliberately; exits 0/1/2)',
|
|
370
|
+
'check-external-deps': 'Does the external-dependency inventory EXIST and carry a verdict per row — an absent section is never «no dependencies» (invoke deliberately; exits 0/1/2)',
|
|
371
|
+
'check-metric-source': 'Does every success metric name WHERE its value comes from, from a closed list, and does an external-API metric have its inventory row (invoke deliberately; exits 0/1/2)',
|
|
363
372
|
},
|
|
364
373
|
},
|
|
365
374
|
// ─── Project-generated groups (created by /replicate Phase 3) ───────────
|
|
@@ -503,6 +512,19 @@ function removeOrphanHooks(existing, oldTemplate, newTemplate) {
|
|
|
503
512
|
return cleaned;
|
|
504
513
|
}
|
|
505
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Canonical form of a hook command for DEDUP ONLY (backlog a1ef0e41 — MEASURED: a user who
|
|
517
|
+
* hand-rewrote `$CLAUDE_PROJECT_DIR` into `${CLAUDE_PROJECT_DIR}` got every event hook DOUBLED on
|
|
518
|
+
* the next update, because the byte-equality Set saw two different strings for one command).
|
|
519
|
+
* Both spellings expand identically in sh, so for identity purposes they ARE the same command:
|
|
520
|
+
* `${VAR}` collapses to `$VAR`, and whitespace runs collapse to one space. The STORED command is
|
|
521
|
+
* never rewritten — the user's own spelling always survives; only the comparison key changes.
|
|
522
|
+
*/
|
|
523
|
+
function canonicalHookCommand(command) {
|
|
524
|
+
if (typeof command !== 'string') return command;
|
|
525
|
+
return command.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, '$$$1').replace(/\s+/g, ' ').trim();
|
|
526
|
+
}
|
|
527
|
+
|
|
506
528
|
function mergeHookMatchers(existing, template) {
|
|
507
529
|
// Clone the existing array; we'll mutate per-entry hook arrays via reference.
|
|
508
530
|
const result = existing.map((m) => ({ ...m, hooks: [...(m.hooks || [])] }));
|
|
@@ -513,10 +535,12 @@ function mergeHookMatchers(existing, template) {
|
|
|
513
535
|
result.push({ ...tplEntry, hooks: [...(tplEntry.hooks || [])] });
|
|
514
536
|
continue;
|
|
515
537
|
}
|
|
516
|
-
// Same matcher — append template hooks not already present
|
|
517
|
-
|
|
538
|
+
// Same matcher — append template hooks not already present. Identity is the CANONICAL
|
|
539
|
+
// command (see canonicalHookCommand), so a user's `${VAR}` respelling of a shipped `$VAR`
|
|
540
|
+
// hook no longer reads as a different hook and doubles on every update.
|
|
541
|
+
const existingCmds = new Set(target.hooks.map((h) => canonicalHookCommand(h.command)));
|
|
518
542
|
for (const tplHook of tplEntry.hooks || []) {
|
|
519
|
-
if (!existingCmds.has(tplHook.command)) {
|
|
543
|
+
if (!existingCmds.has(canonicalHookCommand(tplHook.command))) {
|
|
520
544
|
target.hooks.push(tplHook);
|
|
521
545
|
}
|
|
522
546
|
}
|
|
@@ -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
|
|