bmad-module-skill-forge 2.0.1 → 2.1.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/.claude-plugin/marketplace.json +1 -1
- package/docs/404.md +4 -4
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/RELEASING.md +7 -4
- package/docs/agents.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/bmad-synergy.md +13 -13
- package/docs/campaign.md +3 -3
- package/docs/concepts.md +3 -3
- package/docs/examples.md +9 -9
- package/docs/forge-auto.md +6 -6
- package/docs/getting-started.md +12 -12
- package/docs/how-it-works.md +7 -7
- package/docs/index.md +7 -1
- package/docs/skill-model.md +2 -2
- package/docs/troubleshooting.md +7 -7
- package/docs/why-skf.md +8 -8
- package/docs/workflows.md +6 -6
- package/package.json +7 -3
- package/src/shared/health-check.md +40 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -0
- package/src/shared/scripts/skf-detect-language.py +20 -0
- package/src/shared/scripts/skf-detect-tools.py +34 -0
- package/src/shared/scripts/skf-emit-result-envelope.py +20 -0
- package/src/shared/scripts/skf-language-corpora.py +20 -0
- package/src/shared/scripts/skf-merge-doc-urls.py +20 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +44 -2
- package/src/shared/scripts/skf-recommend-scope-type.py +20 -0
- package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
- package/src/shared/scripts/skf-shape-detect.py +20 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +2 -1
- package/src/skf-analyze-source/references/recommend.md +1 -1
- package/src/skf-audit-skill/references/re-index.md +1 -1
- package/src/skf-brief-skill/assets/description-voice-examples.md +5 -3
- package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
- package/src/skf-brief-skill/references/analyze-target.md +2 -2
- package/src/skf-brief-skill/references/gather-intent.md +4 -2
- package/src/skf-campaign/references/step-05-skill-loop.md +3 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -2
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-create-skill/references/compile.md +12 -3
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +5 -3
- package/src/skf-create-skill/references/step-doc-rot.md +23 -6
- package/src/skf-create-skill/scripts/scan-doc-rot.py +191 -27
- package/src/skf-create-stack-skill/references/detect-integrations.md +1 -1
- package/src/skf-setup/references/ccc-index.md +2 -2
- package/src/skf-setup/references/report.md +1 -1
- package/src/skf-test-skill/references/coverage-check.md +11 -5
- package/src/skf-test-skill/references/score.md +7 -1
- package/src/skf-test-skill/scripts/aggregate-coherence.py +13 -3
- package/src/skf-test-skill/scripts/compute-score.py +17 -4
- package/src/skf-test-skill/scripts/reconcile-coverage.py +36 -9
- package/tools/cli/lib/installer.js +31 -1
- package/tools/validate-docs-links.js +665 -0
|
@@ -32,9 +32,9 @@ For Quick and Forge tiers, or when ccc is unavailable, skip silently and proceed
|
|
|
32
32
|
|
|
33
33
|
Read `{ccc}` and `{ccc_skip_index}` from context.
|
|
34
34
|
|
|
35
|
-
**If `{ccc}` is false:** Set `{ccc_index_result: "none", ccc_indexed_path: null, ccc_last_indexed: null, ccc_exclude_patterns: [], ccc_exclusion_warnings: [], settings_yml_written: false, settings_yml_patterns_added: 0}`. Proceed directly to section
|
|
35
|
+
**If `{ccc}` is false:** Set `{ccc_index_result: "none", ccc_indexed_path: null, ccc_last_indexed: null, ccc_exclude_patterns: [], ccc_exclusion_warnings: [], settings_yml_written: false, settings_yml_patterns_added: 0}`. Proceed directly to section 5 (Auto-Proceed) — no output, no messaging.
|
|
36
36
|
|
|
37
|
-
**If `{ccc}` is true AND `{ccc_skip_index}` is true:** Run the exclusion-merge (section 3) so settings.yml stays current, then set `{ccc_index_result: "skipped", ccc_indexed_path: null, ccc_last_indexed: null}` and proceed to section
|
|
37
|
+
**If `{ccc}` is true AND `{ccc_skip_index}` is true:** Run the exclusion-merge (section 3) so settings.yml stays current, then set `{ccc_index_result: "skipped", ccc_indexed_path: null, ccc_last_indexed: null}` and proceed to section 5 (Auto-Proceed) — do not run `ccc init` or `ccc index`. The envelope's `ccc_index.status` will be `"skipped"` so pipelines that plan an out-of-band re-index can distinguish "the operator opted out" from "indexing failed".
|
|
38
38
|
|
|
39
39
|
**If `{ccc}` is true AND `{ccc_skip_index}` is false:** Continue to section 2.
|
|
40
40
|
|
|
@@ -77,7 +77,7 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
|
|
|
77
77
|
{end if}
|
|
78
78
|
|
|
79
79
|
{if hygiene_result is "qmd_unavailable":}
|
|
80
|
-
QMD Registry: skipped (
|
|
80
|
+
QMD Registry: skipped (qmd unavailable — if the daemon is stopped, `qmd start` restores it).
|
|
81
81
|
{end if}
|
|
82
82
|
|
|
83
83
|
{if tools.ccc is true:}
|
|
@@ -216,9 +216,9 @@ On a split-body skill the §1 inventory (documented surface) and the §2 AST out
|
|
|
216
216
|
|
|
217
217
|
1. **Enumerated path (`denominatorSource: "barrel"`)** — the common split-body case. `barrel_set` is the union of `exports_found[]` across every §2 per-file result. **When a stratified-scope or State-2 denominator applies (see §4) and it resolves to an enumerated name set** — the priority-2/3 re-derivation from `scope.tier_a_include` / `scope.include` globs — pass that resolved name set as `barrelSet` so the script intersects against it instead of the raw per-file union. The script computes `Documented := |documented_set ∩ barrel_set|`, `Missing := barrel_set − documented_set` (in source, not documented), `Stale := documented_set − barrel_set` (documented, not in source), and `Export Coverage = |Documented| / |barrel_set| * 100`.
|
|
218
218
|
|
|
219
|
-
2. **Scalar-denominator branch (`denominatorSource: "scalar"`)** — §4 priority 1, when the resolved denominator is the scalar `metadata.json.stats.effective_denominator` (a count with **no enumerated name set**). There is no `barrel_set` to intersect, so the script instead greps each `documented_set` name across `SKILL.md ∪ references/*.md` and counts appearances. Pass `denominatorValue: effective_denominator` and `skillPackagePath: {resolved_skill_package}`; the script sets `Missing := effective_denominator − Documented` and returns an empty `Stale` (not enumerable without a barrel name set). If §4b's numerator-ground-truth arm fires (it triggers only when `exports_documented == effective_denominator`), its verified count is authoritative and **overrides** this numerator — do not apply both.
|
|
219
|
+
2. **Scalar-denominator branch (`denominatorSource: "scalar"`)** — §4 priority 1, when the resolved denominator is the scalar `metadata.json.stats.effective_denominator` (a count with **no enumerated name set**). There is no `barrel_set` to intersect, so the script instead greps each `documented_set` name across `SKILL.md ∪ references/*.md` and counts appearances. Pass `denominatorValue: effective_denominator` and `skillPackagePath: {resolved_skill_package}`; the script sets `Missing := max(0, effective_denominator − Documented)` and returns an empty `Stale` (not enumerable without a barrel name set). When the grep count overshoots the denominator the script reports the surplus instead of a negative residual — see the surplus note under the returns list. If §4b's numerator-ground-truth arm fires (it triggers only when `exports_documented == effective_denominator`), its verified count is authoritative and **overrides** this numerator — do not apply both.
|
|
220
220
|
|
|
221
|
-
3. **Stack-skill branch (`denominatorSource: "stack"`, `metadata.json.skill_type == "stack"`)** — a stack's source barrel is empty by design, so intersecting against it would divide by zero. Pass `denominatorValue: stack_denominator` (the §2b composition-surface denominator) and `compositionNames`: the provenance-map cited-contract names (`::`-excluded) when the map exists and is non-empty, else the `libraries` and `integration_pairs` names. The script greps each composition name across `SKILL.md ∪ references/*.md` for the numerator, sets `Missing := stack_denominator − Documented`, and omits `Stale` (no source barrel to enumerate against).
|
|
221
|
+
3. **Stack-skill branch (`denominatorSource: "stack"`, `metadata.json.skill_type == "stack"`)** — a stack's source barrel is empty by design, so intersecting against it would divide by zero. Pass `denominatorValue: stack_denominator` (the §2b composition-surface denominator) and `compositionNames`: the provenance-map cited-contract names (`::`-excluded) when the map exists and is non-empty, else the `libraries` and `integration_pairs` names. The script greps each composition name across `SKILL.md ∪ references/*.md` for the numerator, sets `Missing := max(0, stack_denominator − Documented)`, and omits `Stale` (no source barrel to enumerate against). The same surplus reporting as the scalar branch applies.
|
|
222
222
|
|
|
223
223
|
**Build the reconciliation input and run the script:**
|
|
224
224
|
|
|
@@ -242,11 +242,14 @@ Input JSON (one object — supply only the fields the chosen branch needs; `{rec
|
|
|
242
242
|
|
|
243
243
|
The script returns (read these — **do not re-derive them by hand**):
|
|
244
244
|
|
|
245
|
-
- `documented` — the numerator (`|documented_set ∩ barrel_set|` for barrel; grep-verified count for scalar/stack)
|
|
246
|
-
- `missing` / `missingCount` — source names not documented (barrel enumerates the names; scalar/stack give only the residual count)
|
|
245
|
+
- `documented` — the numerator (`|documented_set ∩ barrel_set|` for barrel; grep-verified count for scalar/stack). This is always the true count, never capped.
|
|
246
|
+
- `missing` / `missingCount` — source names not documented (barrel enumerates the names; scalar/stack give only the residual count). Never negative.
|
|
247
247
|
- `stale` / `staleCount` — documented names not in source (barrel only; empty with `staleApplicable: false` for scalar/stack)
|
|
248
248
|
- `denominator` — `|barrel_set|` (barrel) or the resolved scalar/stack denominator
|
|
249
|
-
- `exportCoverage` — `documented / denominator * 100`, already rounded
|
|
249
|
+
- `exportCoverage` — `documented / denominator * 100`, already rounded; capped at 100 on the scalar/stack branches
|
|
250
|
+
- `numeratorSurplus` / `coverageUncapped` / `coverageCapped` — scalar/stack only; see the surplus note below
|
|
251
|
+
|
|
252
|
+
**Surplus on the grep branches.** The scalar and stack branches grep a name set against the skill body, so their numerator and denominator measure independent sets and `documented` can exceed `denominator` — a consumer-surface denominator counts one surface while the documented body may also name migration aliases or re-exported sibling symbols. When that happens the script reports `numeratorSurplus > 0` and `coverageCapped: true`, holds `exportCoverage` at 100, floors `missingCount` at 0, and preserves the raw ratio in `coverageUncapped`. **A surplus is a signal, not a pass:** it means the two sets disagree, so state it in the Coverage Analysis section (§5) alongside both counts. A large surplus on a skill whose brief carries no `scope.tier_a_include` is the deflated-denominator signature the `source-access-protocol.md` deflation guard describes — check that guard before accepting the 100.
|
|
250
253
|
|
|
251
254
|
Carry these into §3's table/summary and §4's Export Coverage, and record the counts in the Coverage Analysis section (§5) so the numerator is auditable. The `exportCoverage` recorded here is the value step 5 feeds to `compute-score.py` — it is the script's value, not a parent estimate.
|
|
252
255
|
|
|
@@ -267,6 +270,8 @@ Aggregate findings across all source files:
|
|
|
267
270
|
- Signature mismatches: {N}
|
|
268
271
|
- Undocumented in SKILL.md but not in source (stale docs): `staleCount`
|
|
269
272
|
|
|
273
|
+
When the script reports `coverageCapped: true`, add the surplus to the summary — `Documented (surplus over denominator): {numeratorSurplus}` and `Uncapped coverage: {coverageUncapped}%` — so the reader can see that `Missing documentation: 0` is a floored residual rather than a verified-complete surface.
|
|
274
|
+
|
|
270
275
|
### 4. Load Scoring Rules
|
|
271
276
|
|
|
272
277
|
Load `{scoringRulesFile}` to determine category scores:
|
|
@@ -390,6 +395,7 @@ Append the **Coverage Analysis** section to `{outputFile}`:
|
|
|
390
395
|
- **Missing Documentation:** {N}
|
|
391
396
|
- **Signature Mismatches:** {N}
|
|
392
397
|
- **Stale Documentation:** {N}
|
|
398
|
+
- **Numerator Surplus:** {N} — omit this row unless `coverageCapped: true`; when present, also give the uncapped percentage
|
|
393
399
|
|
|
394
400
|
### Category Scores
|
|
395
401
|
|
|
@@ -123,7 +123,13 @@ Use these values for Section 4 (pass/fail/inconclusive) and Section 6 (output fo
|
|
|
123
123
|
|
|
124
124
|
#### 3c. Fallback (if script execution fails)
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
**First distinguish "the script could not run" from "the script rejected the input" — they take opposite paths.**
|
|
127
|
+
|
|
128
|
+
A `{"error": ..., "code": "INVALID_INPUT"}` envelope on stdout means the script ran fine and refused what it was given: a field is missing, mistyped, or out of range (exit 2), or the payload was not parseable JSON at all (exit 1). Either way the *input* is wrong, not the script. **Correct the §3a input and re-run.** Do not fall through to the manual redistribution below — it would hand-compute a total from the very numbers the script just refused, so an out-of-range `exportCoverage` would silently become a score. If the input cannot be corrected, report `score: scoring input rejected — {error}` and leave the score unset rather than emitting a computed total.
|
|
129
|
+
|
|
130
|
+
The fallback below applies only when the script genuinely could not run — missing file, no `uv`, unreadable interpreter — that is, **no envelope on stdout at all**.
|
|
131
|
+
|
|
132
|
+
If the script is unavailable, redistribute each skipped category's weight (the `null`-scored categories in the §3a JSON — naive mode already zeroes coherence, and Quick-tier/docsOnly/state2/stackSkill/referenceApp already null out Signature Accuracy + Type Coverage) proportionally across the active categories, then report `total = Σ(weight/100 × category_score)` using the detected mode's weight table in `{scoringRulesFile}`. Report: "**Note:** Scoring script unavailable — calculated manually per scoring-rules.md."
|
|
127
133
|
|
|
128
134
|
### 3d. Read Post-Score Caps (applied by the script)
|
|
129
135
|
|
|
@@ -62,10 +62,14 @@ CLI usage (mirrors compute-score.py):
|
|
|
62
62
|
uv run aggregate-coherence.py --json-input '<JSON>' # explicit flag form
|
|
63
63
|
cat input.json | uv run aggregate-coherence.py --stdin # piped input
|
|
64
64
|
|
|
65
|
-
Exit codes:
|
|
66
|
-
0 —
|
|
67
|
-
object ({"error": ..., "code": "INVALID_INPUT"}) was emitted as JSON
|
|
65
|
+
Exit codes (same convention as compute-score.py / reconcile-coverage.py):
|
|
66
|
+
0 — a result object was emitted
|
|
68
67
|
1 — input could not be parsed at all (no input provided, or malformed JSON)
|
|
68
|
+
2 — input parsed but schema/semantics invalid
|
|
69
|
+
|
|
70
|
+
Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
|
|
71
|
+
so the envelope's presence — not the specific code — tells a caller the input was
|
|
72
|
+
refused rather than scored.
|
|
69
73
|
"""
|
|
70
74
|
|
|
71
75
|
from __future__ import annotations
|
|
@@ -269,6 +273,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
269
273
|
|
|
270
274
|
result = aggregate_coherence(data)
|
|
271
275
|
print(json.dumps(result, indent=2))
|
|
276
|
+
# Same convention as compute-score.py / reconcile-coverage.py. This script
|
|
277
|
+
# produces the coherence percentage that score.md §3a feeds to
|
|
278
|
+
# compute-score.py, so a silently-rejected run here would hand a bogus or
|
|
279
|
+
# absent number to the gate one layer downstream.
|
|
280
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
281
|
+
return 2
|
|
272
282
|
return 0
|
|
273
283
|
|
|
274
284
|
|
|
@@ -39,11 +39,17 @@ Verdict override (post-score caps + threshold fallback — see compute_score):
|
|
|
39
39
|
and `result` is the final verdict. `result` itself is never mutated — it is
|
|
40
40
|
always the pre-cap/pre-fallback score-vs-threshold (or evidence-floor) verdict.
|
|
41
41
|
|
|
42
|
-
Exit codes:
|
|
43
|
-
0 —
|
|
44
|
-
PASS/FAIL/INCONCLUSIVE) or a schema/validation error object
|
|
45
|
-
({"error": ..., "code": "INVALID_INPUT"}) was emitted as JSON
|
|
42
|
+
Exit codes (same convention as reconcile-coverage.py):
|
|
43
|
+
0 — a score was computed; the verdict may be PASS/FAIL/INCONCLUSIVE
|
|
46
44
|
1 — input could not be parsed at all (no input provided, or malformed JSON)
|
|
45
|
+
2 — input parsed but schema/semantics invalid
|
|
46
|
+
|
|
47
|
+
Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
|
|
48
|
+
so the presence of that envelope — not the specific code — is what tells a caller
|
|
49
|
+
the input was refused. A refused input is distinct from an unavailable script:
|
|
50
|
+
the numbers handed in are wrong and must be fixed, NOT hand-computed into a total
|
|
51
|
+
from the values this script declined to score. score.md §3c keys its
|
|
52
|
+
manual-redistribution fallback on that distinction (no envelope at all).
|
|
47
53
|
"""
|
|
48
54
|
|
|
49
55
|
from __future__ import annotations
|
|
@@ -508,6 +514,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
508
514
|
|
|
509
515
|
result = compute_score(data)
|
|
510
516
|
print(json.dumps(result, indent=2))
|
|
517
|
+
# A rejected input exits 2, matching reconcile-coverage.py. Exiting 0 here
|
|
518
|
+
# made a malformed scoring input indistinguishable from a scored run, so the
|
|
519
|
+
# error envelope could be skimmed past and the score hand-computed from the
|
|
520
|
+
# very numbers the script refused. score.md §3c keys the manual-redistribution
|
|
521
|
+
# fallback on this distinction.
|
|
522
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
523
|
+
return 2
|
|
511
524
|
return 0
|
|
512
525
|
|
|
513
526
|
|
|
@@ -22,15 +22,24 @@ It covers the three branches §2c enumerates:
|
|
|
22
22
|
* "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
|
|
23
23
|
grep each documented name across SKILL.md ∪ references/*.md;
|
|
24
24
|
Documented = count present, denominator = effective_denominator,
|
|
25
|
-
Missing = denominator − Documented, Stale not enumerable (empty),
|
|
26
|
-
Export Coverage = Documented / denominator * 100.
|
|
25
|
+
Missing = max(0, denominator − Documented), Stale not enumerable (empty),
|
|
26
|
+
Export Coverage = min(100, Documented / denominator * 100).
|
|
27
27
|
|
|
28
28
|
* "stack" (skill_type == "stack", empty source barrel) — grep each
|
|
29
29
|
composition-surface name (provenance-map cited contracts, ::-excluded, or
|
|
30
30
|
libraries + integration_pairs — resolved upstream) across
|
|
31
31
|
SKILL.md ∪ references/*.md; denominator = stack_denominator,
|
|
32
|
-
Missing = denominator − Documented, Stale not enumerable,
|
|
33
|
-
Export Coverage = Documented / stack_denominator * 100.
|
|
32
|
+
Missing = max(0, denominator − Documented), Stale not enumerable,
|
|
33
|
+
Export Coverage = min(100, Documented / stack_denominator * 100).
|
|
34
|
+
|
|
35
|
+
The two grep branches bound their outputs because the numerator and the
|
|
36
|
+
denominator are independent measures of different sets — the grep count can
|
|
37
|
+
exceed a consumer-surface denominator without either being wrong. Unbounded,
|
|
38
|
+
that produced a negative Missing and a >100% coverage that compute-score.py
|
|
39
|
+
rejects as out of range. `documented` is left as the true count (it is reported
|
|
40
|
+
verbatim as "Documented in SKILL.md"); the derived ratio is what gets bounded,
|
|
41
|
+
and `numeratorSurplus` / `coverageUncapped` / `coverageCapped` report the
|
|
42
|
+
overshoot so a deflated denominator stays visible instead of being swallowed.
|
|
34
43
|
|
|
35
44
|
CLI usage:
|
|
36
45
|
uv run reconcile-coverage.py '<JSON>' # JSON literal positional
|
|
@@ -55,11 +64,14 @@ Output (stdout, one object):
|
|
|
55
64
|
"denominator": <int>,
|
|
56
65
|
"documented": <int>,
|
|
57
66
|
"missing": [names], # enumerated: source names not documented; scalar/stack: []
|
|
58
|
-
"missingCount": <int>, # always present
|
|
67
|
+
"missingCount": <int>, # always present; never negative
|
|
59
68
|
"stale": [names], # enumerated: documented names not in source; scalar/stack: []
|
|
60
69
|
"staleCount": <int>, # always present
|
|
61
70
|
"staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
|
|
62
|
-
"exportCoverage": <float
|
|
71
|
+
"exportCoverage": <float>, # scalar/stack: capped at 100
|
|
72
|
+
"numeratorSurplus": <int>, # scalar/stack only: max(0, documented − denominator)
|
|
73
|
+
"coverageUncapped": <float>, # scalar/stack only: the ratio before the cap
|
|
74
|
+
"coverageCapped": <bool> # scalar/stack only: true when the cap bound the ratio
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
Exit codes:
|
|
@@ -232,18 +244,33 @@ def reconcile(inp, doc_text=None):
|
|
|
232
244
|
"upstream §2b guard should HALT before reconciliation"
|
|
233
245
|
)
|
|
234
246
|
|
|
235
|
-
|
|
247
|
+
# The grep numerator and the resolved denominator are independent measures,
|
|
248
|
+
# so `documented` can legitimately exceed `denominator` — a consumer-surface
|
|
249
|
+
# denominator counts one surface while the documented body may also name
|
|
250
|
+
# migration aliases, re-exported sibling symbols, and other extras. Left
|
|
251
|
+
# unbounded that yields a negative Missing and >100% coverage (which
|
|
252
|
+
# compute-score.py then rejects outright as out of range).
|
|
253
|
+
#
|
|
254
|
+
# `documented` stays the true grep count: it is reported as "Documented in
|
|
255
|
+
# SKILL.md", so capping it would print a number that was never measured.
|
|
256
|
+
# The derived ratio is what gets bounded, and the surplus is surfaced rather
|
|
257
|
+
# than swallowed so a deflated denominator stays visible.
|
|
258
|
+
surplus = max(0, documented - denominator)
|
|
259
|
+
coverage_uncapped = round2(documented / denominator * 100)
|
|
236
260
|
return {
|
|
237
261
|
"branch": source,
|
|
238
262
|
"denominatorSource": source,
|
|
239
263
|
"denominator": denominator,
|
|
240
264
|
"documented": documented,
|
|
241
265
|
"missing": [],
|
|
242
|
-
"missingCount":
|
|
266
|
+
"missingCount": max(0, denominator - documented),
|
|
267
|
+
"numeratorSurplus": surplus,
|
|
243
268
|
"stale": [],
|
|
244
269
|
"staleCount": 0,
|
|
245
270
|
"staleApplicable": False,
|
|
246
|
-
"exportCoverage":
|
|
271
|
+
"exportCoverage": min(100.0, coverage_uncapped),
|
|
272
|
+
"coverageUncapped": coverage_uncapped,
|
|
273
|
+
"coverageCapped": surplus > 0,
|
|
247
274
|
}
|
|
248
275
|
|
|
249
276
|
|
|
@@ -205,10 +205,39 @@ class Installer {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Read the health_check_repo default from src/module.yaml (single source of truth).
|
|
210
|
+
*/
|
|
211
|
+
async readHealthCheckRepoDefault() {
|
|
212
|
+
try {
|
|
213
|
+
const moduleData = yaml.load(await fs.readFile(path.join(this.srcDir, 'module.yaml'), 'utf8'));
|
|
214
|
+
const value = moduleData && moduleData.health_check_repo && moduleData.health_check_repo.default;
|
|
215
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
216
|
+
} catch {
|
|
217
|
+
/* fall through to literal fallback */
|
|
218
|
+
}
|
|
219
|
+
return 'armelhbobdad/bmad-module-skill-forge';
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
async writeConfig(skfDir, config) {
|
|
209
223
|
// On update, restore the user's existing config
|
|
210
224
|
if (config._savedConfigYaml) {
|
|
211
|
-
|
|
225
|
+
let savedYaml = config._savedConfigYaml;
|
|
226
|
+
try {
|
|
227
|
+
// Inject health_check_repo into configs generated before it existed.
|
|
228
|
+
// Append one line instead of re-dumping so user comments/ordering survive.
|
|
229
|
+
// Only mapping-shaped (or empty/comments-only) docs are appendable —
|
|
230
|
+
// appending a key to an array or scalar doc would corrupt the YAML.
|
|
231
|
+
const savedData = yaml.load(savedYaml);
|
|
232
|
+
const isPlainObject = typeof savedData === 'object' && savedData !== null && !Array.isArray(savedData);
|
|
233
|
+
if (savedData == null || (isPlainObject && !('health_check_repo' in savedData))) {
|
|
234
|
+
const suffix = savedYaml.endsWith('\n') ? '' : '\n';
|
|
235
|
+
savedYaml = `${savedYaml}${suffix}health_check_repo: ${await this.readHealthCheckRepoDefault()}\n`;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
/* unparseable saved config — restore verbatim */
|
|
239
|
+
}
|
|
240
|
+
await fs.writeFile(path.join(skfDir, 'config.yaml'), savedYaml, 'utf8');
|
|
212
241
|
return;
|
|
213
242
|
}
|
|
214
243
|
|
|
@@ -232,6 +261,7 @@ class Installer {
|
|
|
232
261
|
forge_data_folder: config.forge_data_folder || 'forge-data',
|
|
233
262
|
sidecar_path: '_bmad/_memory/forger-sidecar',
|
|
234
263
|
skf_folder: config.skfFolder,
|
|
264
|
+
health_check_repo: await this.readHealthCheckRepoDefault(),
|
|
235
265
|
ides: config.ides || [],
|
|
236
266
|
install_learning: config.install_learning !== false,
|
|
237
267
|
};
|