bmad-module-skill-forge 2.0.0 → 2.0.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/.claude-plugin/marketplace.json +1 -1
- package/.nvmrc +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +34 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +59 -28
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +51 -31
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +42 -10
- package/src/skf-drop-skill/references/select.md +88 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +27 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +13 -10
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +107 -78
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +109 -75
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +38 -20
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +131 -1
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +12 -24
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
1
2
|
# /// script
|
|
2
3
|
# requires-python = ">=3.10"
|
|
3
4
|
# dependencies = []
|
|
@@ -25,9 +26,19 @@ Input schema (one object):
|
|
|
25
26
|
"externalValidation": <0-100>
|
|
26
27
|
},
|
|
27
28
|
"threshold": <0-100, optional, default 80>,
|
|
28
|
-
"evidenceCount": <int, optional, used for INCONCLUSIVE floor
|
|
29
|
+
"evidenceCount": <int, optional, used for INCONCLUSIVE floor>,
|
|
30
|
+
"analysisConfidence": "<optional string; 'degraded' fires the tooling cap>",
|
|
31
|
+
"toolingStatus": "<optional string; a '*-missing' marker fires the tooling cap>"
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
Verdict override (post-score caps + threshold fallback — see compute_score):
|
|
35
|
+
When a cap or the threshold fallback engages, the output additionally carries
|
|
36
|
+
`effectiveResult` (the final verdict after caps/fallback), `capReason`
|
|
37
|
+
(string or null), `thresholdFallback` (bool), and `originalThreshold` (the
|
|
38
|
+
pre-fallback threshold, or null). When neither engages, those keys are omitted
|
|
39
|
+
and `result` is the final verdict. `result` itself is never mutated — it is
|
|
40
|
+
always the pre-cap/pre-fallback score-vs-threshold (or evidence-floor) verdict.
|
|
41
|
+
|
|
31
42
|
Exit codes:
|
|
32
43
|
0 — input was parsed; either a score was computed (verdict may be
|
|
33
44
|
PASS/FAIL/INCONCLUSIVE) or a schema/validation error object
|
|
@@ -71,6 +82,59 @@ CATEGORIES = [
|
|
|
71
82
|
DEFAULT_THRESHOLD = 80
|
|
72
83
|
|
|
73
84
|
|
|
85
|
+
# --- Redistribution equivalence classes (moved here from scoring-rules.md) ---
|
|
86
|
+
#
|
|
87
|
+
# Determinism note for maintainers — NOT reader-model context. The redistributed
|
|
88
|
+
# `weights` / `activeCategories` / `skippedCategories` output depends ONLY on
|
|
89
|
+
# three things: the base table (contextual vs naive), whether Signature Accuracy
|
|
90
|
+
# + Type Coverage are skipped (Quick tier OR docsOnly OR state2 OR stackSkill OR
|
|
91
|
+
# referenceApp), and whether External Validation is skipped (externalValidation
|
|
92
|
+
# is null). Every (mode × tier × docsOnly × state2 × stackSkill × referenceApp)
|
|
93
|
+
# cell with an identical skip set + base table therefore reduces to the same
|
|
94
|
+
# equivalence class below and emits identical final weights for identical input
|
|
95
|
+
# scores — only the `skipReasons` string differs. The prompt (score.md) never
|
|
96
|
+
# names a class or fixture; it sets the flags and reads the script's output back.
|
|
97
|
+
# The rightmost column pins each class to a representative fixture in
|
|
98
|
+
# test/fixtures/compute-score-contract.json (see test-compute-score-contract.py);
|
|
99
|
+
# "(equiv.)" cells reduce algebraically to a listed representative.
|
|
100
|
+
#
|
|
101
|
+
# | # | mode | tier | docsOnly | state2 | base | sig/type skip | ext skip | class | representative fixture |
|
|
102
|
+
# |----|------------|--------|----------|--------|------------|------------------------|----------|-------|----------------------------------|
|
|
103
|
+
# | 1 | contextual | Deep | F | F | contextual | — | if null | A | suite_a_all_active |
|
|
104
|
+
# | 2 | contextual | Forge+ | F | F | contextual | — | if null | A | suite_k_forge_plus |
|
|
105
|
+
# | 3 | contextual | Forge | F | F | contextual | — | if null | A | suite_p_contextual_forge |
|
|
106
|
+
# | 4 | contextual | Quick | F | F | contextual | Quick tier | if null | B | suite_c_quick_tier |
|
|
107
|
+
# | 5 | contextual | Deep | T | F | contextual | docs-only | if null | B | suite_r_contextual_deep_docsonly |
|
|
108
|
+
# | 6 | contextual | Forge+ | T | F | contextual | docs-only | if null | B | (equiv.) |
|
|
109
|
+
# | 7 | contextual | Forge | T | F | contextual | docs-only | if null | B | (equiv.) |
|
|
110
|
+
# | 8 | contextual | Quick | T | F | contextual | Quick tier + docs-only | if null | B | suite_f_docs_only |
|
|
111
|
+
# | 9 | contextual | Deep | F | T | contextual | State 2 | if null | B | suite_g_state2 |
|
|
112
|
+
# | 10 | contextual | Forge+ | F | T | contextual | State 2 | if null | B | (equiv.) |
|
|
113
|
+
# | 11 | contextual | Forge | F | T | contextual | State 2 | if null | B | (equiv.) |
|
|
114
|
+
# | 12 | contextual | Quick | F | T | contextual | Quick + State 2 | if null | B | (equiv.) |
|
|
115
|
+
# | 13 | contextual | * | T | T | contextual | docs-only + State 2 | if null | B | (equiv.) |
|
|
116
|
+
# | 14 | naive | Deep | F | F | naive | — | if null | C | suite_q_naive_deep |
|
|
117
|
+
# | 15 | naive | Forge+ | F | F | naive | — | if null | C | (equiv.) |
|
|
118
|
+
# | 16 | naive | Forge | F | F | naive | — | if null | C | suite_b_naive |
|
|
119
|
+
# | 17 | naive | Quick | F | F | naive | Quick tier | if null | D | suite_e_triple_skip |
|
|
120
|
+
# | 18 | naive | Deep | T | F | naive | docs-only | if null | D | suite_s_naive_deep_docsonly |
|
|
121
|
+
# | 19 | naive | Forge+ | T | F | naive | docs-only | if null | D | (equiv.) |
|
|
122
|
+
# | 20 | naive | Forge | T | F | naive | docs-only | if null | D | (equiv.) |
|
|
123
|
+
# | 21 | naive | Quick | T | F | naive | Quick + docs-only | if null | D | suite_o_input_echo |
|
|
124
|
+
# | 22 | naive | Deep | F | T | naive | State 2 | if null | D | suite_t_naive_state2 |
|
|
125
|
+
# | 23 | naive | Forge+ | F | T | naive | State 2 | if null | D | (equiv.) |
|
|
126
|
+
# | 24 | naive | Forge | F | T | naive | State 2 | if null | D | (equiv.) |
|
|
127
|
+
# | 25 | naive | Quick | F | T | naive | Quick + State 2 | if null | D | (equiv.) |
|
|
128
|
+
# | 26 | naive | * | T | T | naive | docs-only + State 2 | if null | D | (equiv.) |
|
|
129
|
+
#
|
|
130
|
+
# Stack skills (stackSkill) and reference-app skills (referenceApp) share the
|
|
131
|
+
# skip set of the docsOnly/state2 rows: contextual → class B, naive → class D;
|
|
132
|
+
# only skipReasons differs (fixtures suite_u_stack_skill_deep,
|
|
133
|
+
# suite_v_reference_app_deep). Quick-tier rows (4, 8, 12, 17, 21, 25) then feed
|
|
134
|
+
# the minimum-evidence floor in the result block below (§7 of compute_score),
|
|
135
|
+
# which can force INCONCLUSIVE after this pre-floor redistribution.
|
|
136
|
+
|
|
137
|
+
|
|
74
138
|
# --- Helpers ---
|
|
75
139
|
|
|
76
140
|
|
|
@@ -130,6 +194,11 @@ def validate_input(inp):
|
|
|
130
194
|
if not isinstance(threshold, (int, float)) or threshold < 0 or threshold > 100:
|
|
131
195
|
return "threshold must be a number between 0 and 100"
|
|
132
196
|
|
|
197
|
+
for str_field in ("analysisConfidence", "toolingStatus"):
|
|
198
|
+
val = inp.get(str_field)
|
|
199
|
+
if val is not None and not isinstance(val, str):
|
|
200
|
+
return f"{str_field} must be a string or null, got: {type(val).__name__}"
|
|
201
|
+
|
|
133
202
|
for cat in CATEGORIES:
|
|
134
203
|
score = inp["scores"].get(cat)
|
|
135
204
|
if score is not None:
|
|
@@ -277,6 +346,58 @@ def compute_score(inp):
|
|
|
277
346
|
else:
|
|
278
347
|
result = "PASS" if total_score >= threshold else "FAIL"
|
|
279
348
|
|
|
349
|
+
# 7b. Post-score caps + threshold fallback — deterministic verdict override.
|
|
350
|
+
# Lifted from score.md §3d/§4b so the cap<->fallback interaction is centralized
|
|
351
|
+
# and unit-tested here rather than re-derived in the prompt. The minimum-
|
|
352
|
+
# evidence floor (INCONCLUSIVE) is a gate that NO cap or fallback may override,
|
|
353
|
+
# so all of this is skipped when result == "INCONCLUSIVE".
|
|
354
|
+
analysis_confidence = inp.get("analysisConfidence")
|
|
355
|
+
tooling_status = inp.get("toolingStatus")
|
|
356
|
+
|
|
357
|
+
effective_result = result
|
|
358
|
+
cap_reason = None
|
|
359
|
+
threshold_fallback = False
|
|
360
|
+
original_threshold = None
|
|
361
|
+
cap_fired = False
|
|
362
|
+
|
|
363
|
+
if result != "INCONCLUSIVE":
|
|
364
|
+
cap_reasons = []
|
|
365
|
+
# Cap 1 — tooling degraded: analysis confidence is "degraded", or a
|
|
366
|
+
# missing-helper marker (e.g. python3-missing, frontmatter-validator-missing).
|
|
367
|
+
tooling_degraded = analysis_confidence == "degraded" or (
|
|
368
|
+
isinstance(tooling_status, str) and "missing" in tooling_status
|
|
369
|
+
)
|
|
370
|
+
if tooling_degraded:
|
|
371
|
+
cap_fired = True
|
|
372
|
+
cap_reasons.append(
|
|
373
|
+
"tooling degraded — capped below threshold until helper restored"
|
|
374
|
+
)
|
|
375
|
+
# Cap 2 — docs-only mode with no external validators available.
|
|
376
|
+
if docs_only and scores.get("externalValidation") is None:
|
|
377
|
+
cap_fired = True
|
|
378
|
+
cap_reasons.append(
|
|
379
|
+
"docs-only without external validators — capped below threshold"
|
|
380
|
+
)
|
|
381
|
+
if cap_fired:
|
|
382
|
+
cap_reason = "; ".join(cap_reasons)
|
|
383
|
+
# A cap only flips a PASS into FAIL; a pre-existing FAIL stays FAIL.
|
|
384
|
+
if result == "PASS":
|
|
385
|
+
effective_result = "FAIL"
|
|
386
|
+
|
|
387
|
+
# Threshold fallback — convert a FAIL into PASS at the 80 floor when the
|
|
388
|
+
# RAW totalScore clears 80 and the effective threshold was above 80. The
|
|
389
|
+
# fallback reads the raw totalScore (matching score.md §4b's documented
|
|
390
|
+
# `totalScore >= 80`), not the capped value; because it also requires
|
|
391
|
+
# threshold > 80 (so threshold - 1 >= 80), min(totalScore, threshold - 1)
|
|
392
|
+
# is itself >= 80 whenever totalScore >= 80, so raw-vs-capped is the same
|
|
393
|
+
# decision — raw is used for spec fidelity and testability.
|
|
394
|
+
if effective_result == "FAIL" and total_score >= 80 and threshold > 80:
|
|
395
|
+
threshold_fallback = True
|
|
396
|
+
original_threshold = threshold
|
|
397
|
+
effective_result = "PASS"
|
|
398
|
+
|
|
399
|
+
overrides_engaged = cap_fired or threshold_fallback
|
|
400
|
+
|
|
280
401
|
# Build scores echo with null preservation
|
|
281
402
|
scores_echo = {}
|
|
282
403
|
for cat in CATEGORIES:
|
|
@@ -303,6 +424,15 @@ def compute_score(inp):
|
|
|
303
424
|
"weightSum": weight_sum,
|
|
304
425
|
}
|
|
305
426
|
|
|
427
|
+
# Verdict-override fields — emitted as an atomic group only when a post-score
|
|
428
|
+
# cap or the threshold fallback engaged (mirrors the conditional `warnings` /
|
|
429
|
+
# `inconclusiveReasons` fields). When absent, `result` is the final verdict.
|
|
430
|
+
if overrides_engaged:
|
|
431
|
+
output["effectiveResult"] = effective_result
|
|
432
|
+
output["capReason"] = cap_reason
|
|
433
|
+
output["thresholdFallback"] = threshold_fallback
|
|
434
|
+
output["originalThreshold"] = original_threshold
|
|
435
|
+
|
|
306
436
|
if warnings:
|
|
307
437
|
output["warnings"] = warnings
|
|
308
438
|
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# ///
|
|
5
|
+
"""Deterministic Documented-vs-Source coverage reconciliation.
|
|
6
|
+
|
|
7
|
+
Set-reconciliation helper for the SKF test-skill workflow (coverage-check.md
|
|
8
|
+
§2c). Distinct from compute-score.py (which owns weight tables, redistribution,
|
|
9
|
+
and the INCONCLUSIVE floor): this script performs the coverage *numerator*
|
|
10
|
+
arithmetic that §2c calls the "Deterministic Intersection" — turning two
|
|
11
|
+
already-extracted name lists (the §1 documented inventory and the §2 source
|
|
12
|
+
barrel) into a reproducible {documented, missing, stale, exportCoverage}
|
|
13
|
+
result, so the numerator no longer swings between runs on split-body skills.
|
|
14
|
+
|
|
15
|
+
It covers the three branches §2c enumerates:
|
|
16
|
+
|
|
17
|
+
* "barrel" (enumerated path) — intersect documented_set against the source
|
|
18
|
+
barrel_set: Documented = |documented_set ∩ barrel_set|,
|
|
19
|
+
Missing = barrel_set − documented_set, Stale = documented_set − barrel_set,
|
|
20
|
+
Export Coverage = |Documented| / |barrel_set| * 100.
|
|
21
|
+
|
|
22
|
+
* "scalar" (§4 priority-1 effective_denominator, no enumerated name set) —
|
|
23
|
+
grep each documented name across SKILL.md ∪ references/*.md;
|
|
24
|
+
Documented = count present, denominator = effective_denominator,
|
|
25
|
+
Missing = denominator − Documented, Stale not enumerable (empty),
|
|
26
|
+
Export Coverage = Documented / denominator * 100.
|
|
27
|
+
|
|
28
|
+
* "stack" (skill_type == "stack", empty source barrel) — grep each
|
|
29
|
+
composition-surface name (provenance-map cited contracts, ::-excluded, or
|
|
30
|
+
libraries + integration_pairs — resolved upstream) across
|
|
31
|
+
SKILL.md ∪ references/*.md; denominator = stack_denominator,
|
|
32
|
+
Missing = denominator − Documented, Stale not enumerable,
|
|
33
|
+
Export Coverage = Documented / stack_denominator * 100.
|
|
34
|
+
|
|
35
|
+
CLI usage:
|
|
36
|
+
uv run reconcile-coverage.py '<JSON>' # JSON literal positional
|
|
37
|
+
uv run reconcile-coverage.py --json-input '<JSON>' # explicit flag form
|
|
38
|
+
cat input.json | uv run reconcile-coverage.py --stdin # piped input
|
|
39
|
+
|
|
40
|
+
Input schema (one object):
|
|
41
|
+
{
|
|
42
|
+
"denominatorSource": "barrel" | "scalar" | "stack", # required
|
|
43
|
+
"exports": [ {"name": "...", "kind": "..."}, ... ], # §1 inventory
|
|
44
|
+
"barrelSet": ["a", "b", ...], # barrel: resolved name set
|
|
45
|
+
"perFileResults": [ {"exports_found": ["a", ...]} ], # barrel: union'd if no barrelSet
|
|
46
|
+
"denominatorValue": <int>, # scalar/stack: resolved denominator
|
|
47
|
+
"compositionNames": ["lib::x", ...], # stack: names to grep
|
|
48
|
+
"skillPackagePath": "/path/to/skill" # scalar/stack: SKILL.md ∪ references
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Output (stdout, one object):
|
|
52
|
+
{
|
|
53
|
+
"branch": "enumerated" | "scalar" | "stack",
|
|
54
|
+
"denominatorSource": "barrel" | "scalar" | "stack",
|
|
55
|
+
"denominator": <int>,
|
|
56
|
+
"documented": <int>,
|
|
57
|
+
"missing": [names], # enumerated: source names not documented; scalar/stack: []
|
|
58
|
+
"missingCount": <int>, # always present
|
|
59
|
+
"stale": [names], # enumerated: documented names not in source; scalar/stack: []
|
|
60
|
+
"staleCount": <int>, # always present
|
|
61
|
+
"staleApplicable": <bool>, # false for scalar/stack (no barrel to enumerate)
|
|
62
|
+
"exportCoverage": <float>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 — reconciliation emitted successfully
|
|
67
|
+
1 — no input / input could not be parsed as JSON
|
|
68
|
+
2 — input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
from __future__ import annotations
|
|
72
|
+
|
|
73
|
+
import argparse
|
|
74
|
+
import json
|
|
75
|
+
import math
|
|
76
|
+
import sys
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
|
|
79
|
+
VALID_SOURCES = ("barrel", "scalar", "stack")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def round2(value):
|
|
83
|
+
"""Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
|
|
84
|
+
return math.floor(value * 100 + 0.5) / 100
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def make_error(message):
|
|
88
|
+
return {"error": message, "code": "INVALID_INPUT"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# --- Documented-set derivation ---------------------------------------------
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_documented_set(exports):
|
|
95
|
+
"""De-duplicated set of `name` from the §1 inventory, excluding kind:"method".
|
|
96
|
+
|
|
97
|
+
Methods are members of an already-counted class/type, not top-level barrel
|
|
98
|
+
exports (coverage-check.md §2c step 1).
|
|
99
|
+
"""
|
|
100
|
+
names = set()
|
|
101
|
+
for entry in exports or []:
|
|
102
|
+
if not isinstance(entry, dict):
|
|
103
|
+
continue
|
|
104
|
+
if entry.get("kind") == "method":
|
|
105
|
+
continue
|
|
106
|
+
name = entry.get("name")
|
|
107
|
+
if isinstance(name, str) and name:
|
|
108
|
+
names.add(name)
|
|
109
|
+
return names
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_barrel_set(inp):
|
|
113
|
+
"""barrel_set = explicit `barrelSet` if given, else union of exports_found[]."""
|
|
114
|
+
if inp.get("barrelSet") is not None:
|
|
115
|
+
return {n for n in inp["barrelSet"] if isinstance(n, str) and n}
|
|
116
|
+
barrel = set()
|
|
117
|
+
for result in inp.get("perFileResults") or []:
|
|
118
|
+
if not isinstance(result, dict):
|
|
119
|
+
continue
|
|
120
|
+
for name in result.get("exports_found") or []:
|
|
121
|
+
if isinstance(name, str) and name:
|
|
122
|
+
barrel.add(name)
|
|
123
|
+
return barrel
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --- Grep (documented-name presence) ---------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_doc_text(skill_package_path):
|
|
130
|
+
"""Concatenate SKILL.md ∪ references/*.md text for name-presence grepping.
|
|
131
|
+
|
|
132
|
+
Deterministic: references are read in sorted order. Reads as UTF-8 so
|
|
133
|
+
non-ASCII exports do not mojibake on Windows (cp1252 default).
|
|
134
|
+
"""
|
|
135
|
+
root = Path(skill_package_path)
|
|
136
|
+
parts = []
|
|
137
|
+
skill_md = root / "SKILL.md"
|
|
138
|
+
if skill_md.is_file():
|
|
139
|
+
parts.append(skill_md.read_text(encoding="utf-8"))
|
|
140
|
+
refs_dir = root / "references"
|
|
141
|
+
if refs_dir.is_dir():
|
|
142
|
+
for ref in sorted(refs_dir.glob("*.md")):
|
|
143
|
+
parts.append(ref.read_text(encoding="utf-8"))
|
|
144
|
+
return "\n".join(parts)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def names_present(names, doc_text):
|
|
148
|
+
"""Return the sorted, de-duplicated set of `names` that appear in doc_text.
|
|
149
|
+
|
|
150
|
+
Substring containment, case-sensitive — mirrors `grep "{name}"`.
|
|
151
|
+
"""
|
|
152
|
+
present = {n for n in names if isinstance(n, str) and n and n in doc_text}
|
|
153
|
+
return sorted(present)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- Core reconciliation ----------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate(inp):
|
|
160
|
+
if inp is None or not isinstance(inp, dict):
|
|
161
|
+
return "Input must be a JSON object"
|
|
162
|
+
source = inp.get("denominatorSource")
|
|
163
|
+
if source not in VALID_SOURCES:
|
|
164
|
+
return (
|
|
165
|
+
"Missing or invalid required field: denominatorSource "
|
|
166
|
+
f"(must be one of: {', '.join(VALID_SOURCES)})"
|
|
167
|
+
)
|
|
168
|
+
if source == "barrel":
|
|
169
|
+
if inp.get("barrelSet") is None and inp.get("perFileResults") is None:
|
|
170
|
+
return "barrel branch requires either `barrelSet` or `perFileResults`"
|
|
171
|
+
else: # scalar / stack
|
|
172
|
+
dv = inp.get("denominatorValue")
|
|
173
|
+
if not isinstance(dv, int) or isinstance(dv, bool) or dv < 0:
|
|
174
|
+
return f"{source} branch requires integer `denominatorValue` >= 0"
|
|
175
|
+
if not inp.get("skillPackagePath"):
|
|
176
|
+
return f"{source} branch requires `skillPackagePath` to grep SKILL.md ∪ references"
|
|
177
|
+
if source == "stack" and inp.get("compositionNames") is None:
|
|
178
|
+
return "stack branch requires `compositionNames` (composition-surface names to grep)"
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def reconcile(inp, doc_text=None):
|
|
183
|
+
"""Pure reconciliation. `doc_text` may be injected (tests); else loaded from disk."""
|
|
184
|
+
err = _validate(inp)
|
|
185
|
+
if err:
|
|
186
|
+
return make_error(err)
|
|
187
|
+
|
|
188
|
+
source = inp["denominatorSource"]
|
|
189
|
+
|
|
190
|
+
if source == "barrel":
|
|
191
|
+
documented_set = build_documented_set(inp.get("exports"))
|
|
192
|
+
barrel_set = build_barrel_set(inp)
|
|
193
|
+
if not barrel_set:
|
|
194
|
+
return make_error(
|
|
195
|
+
"barrel branch: barrel_set is empty (denominator 0) — "
|
|
196
|
+
"Export Coverage is undefined; upstream §2b zero-exports guard should HALT"
|
|
197
|
+
)
|
|
198
|
+
documented_names = documented_set & barrel_set
|
|
199
|
+
missing = sorted(barrel_set - documented_set)
|
|
200
|
+
stale = sorted(documented_set - barrel_set)
|
|
201
|
+
denominator = len(barrel_set)
|
|
202
|
+
documented = len(documented_names)
|
|
203
|
+
return {
|
|
204
|
+
"branch": "enumerated",
|
|
205
|
+
"denominatorSource": source,
|
|
206
|
+
"denominator": denominator,
|
|
207
|
+
"documented": documented,
|
|
208
|
+
"missing": missing,
|
|
209
|
+
"missingCount": len(missing),
|
|
210
|
+
"stale": stale,
|
|
211
|
+
"staleCount": len(stale),
|
|
212
|
+
"staleApplicable": True,
|
|
213
|
+
"exportCoverage": round2(documented / denominator * 100),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# scalar / stack — grep-based numerator against SKILL.md ∪ references
|
|
217
|
+
denominator = inp["denominatorValue"]
|
|
218
|
+
if doc_text is None:
|
|
219
|
+
doc_text = load_doc_text(inp["skillPackagePath"])
|
|
220
|
+
|
|
221
|
+
if source == "scalar":
|
|
222
|
+
candidates = sorted(build_documented_set(inp.get("exports")))
|
|
223
|
+
else: # stack
|
|
224
|
+
candidates = [n for n in inp["compositionNames"] if isinstance(n, str) and n]
|
|
225
|
+
|
|
226
|
+
present = names_present(candidates, doc_text)
|
|
227
|
+
documented = len(present)
|
|
228
|
+
|
|
229
|
+
if denominator == 0:
|
|
230
|
+
return make_error(
|
|
231
|
+
f"{source} branch: denominatorValue is 0 — Export Coverage is undefined; "
|
|
232
|
+
"upstream §2b guard should HALT before reconciliation"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
missing_count = denominator - documented
|
|
236
|
+
return {
|
|
237
|
+
"branch": source,
|
|
238
|
+
"denominatorSource": source,
|
|
239
|
+
"denominator": denominator,
|
|
240
|
+
"documented": documented,
|
|
241
|
+
"missing": [],
|
|
242
|
+
"missingCount": missing_count,
|
|
243
|
+
"stale": [],
|
|
244
|
+
"staleCount": 0,
|
|
245
|
+
"staleApplicable": False,
|
|
246
|
+
"exportCoverage": round2(documented / denominator * 100),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --- CLI --------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_parser():
|
|
254
|
+
parser = argparse.ArgumentParser(
|
|
255
|
+
prog="reconcile-coverage",
|
|
256
|
+
description=(
|
|
257
|
+
"Deterministic Documented-vs-Source coverage reconciliation "
|
|
258
|
+
"(coverage-check.md §2c). Consumes the §1 documented inventory and "
|
|
259
|
+
"the §2 source barrel (or a resolved scalar/stack denominator + the "
|
|
260
|
+
"skill package path) and emits {documented, missing, stale, "
|
|
261
|
+
"exportCoverage} so the coverage numerator is reproducible."
|
|
262
|
+
),
|
|
263
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
264
|
+
epilog=(
|
|
265
|
+
"Example (barrel branch):\n"
|
|
266
|
+
" uv run reconcile-coverage.py "
|
|
267
|
+
"'{\"denominatorSource\":\"barrel\","
|
|
268
|
+
"\"exports\":[{\"name\":\"a\",\"kind\":\"function\"}],"
|
|
269
|
+
"\"barrelSet\":[\"a\",\"b\"]}'"
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
src = parser.add_mutually_exclusive_group()
|
|
273
|
+
src.add_argument(
|
|
274
|
+
"json_input",
|
|
275
|
+
nargs="?",
|
|
276
|
+
help="JSON object as a positional argument (single-quote it on the shell).",
|
|
277
|
+
)
|
|
278
|
+
src.add_argument(
|
|
279
|
+
"--json-input",
|
|
280
|
+
dest="json_input_flag",
|
|
281
|
+
help="JSON object passed via flag (overrides positional).",
|
|
282
|
+
)
|
|
283
|
+
src.add_argument(
|
|
284
|
+
"--stdin",
|
|
285
|
+
action="store_true",
|
|
286
|
+
help="Read the JSON object from stdin.",
|
|
287
|
+
)
|
|
288
|
+
return parser
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _resolve_input(args):
|
|
292
|
+
if args.stdin:
|
|
293
|
+
return sys.stdin.read()
|
|
294
|
+
if args.json_input_flag is not None:
|
|
295
|
+
return args.json_input_flag
|
|
296
|
+
if args.json_input is not None:
|
|
297
|
+
return args.json_input
|
|
298
|
+
return ""
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main(argv=None):
|
|
302
|
+
parser = _build_parser()
|
|
303
|
+
args = parser.parse_args(argv)
|
|
304
|
+
raw = _resolve_input(args)
|
|
305
|
+
if not raw.strip():
|
|
306
|
+
parser.print_usage(file=sys.stderr)
|
|
307
|
+
print(
|
|
308
|
+
"error: no input provided (positional arg, --json-input, or --stdin)",
|
|
309
|
+
file=sys.stderr,
|
|
310
|
+
)
|
|
311
|
+
return 1
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
data = json.loads(raw)
|
|
315
|
+
except json.JSONDecodeError as exc:
|
|
316
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
317
|
+
return 1
|
|
318
|
+
|
|
319
|
+
result = reconcile(data)
|
|
320
|
+
print(json.dumps(result, indent=2))
|
|
321
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
322
|
+
return 2
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
raise SystemExit(main())
|