bmad-module-skill-forge 1.9.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/README.md +10 -5
- package/docs/_data/pinned.yaml +2 -2
- package/docs/agents.md +11 -2
- package/docs/architecture.md +5 -4
- package/docs/bmad-synergy.md +30 -7
- package/docs/campaign.md +172 -0
- package/docs/examples.md +7 -3
- package/docs/forge-auto.md +90 -0
- package/docs/getting-started.md +9 -0
- package/docs/how-it-works.md +6 -4
- package/docs/index.md +4 -3
- package/docs/skill-model.md +1 -1
- package/docs/troubleshooting.md +17 -1
- package/docs/verifying-a-skill.md +2 -2
- package/docs/why-skf.md +1 -1
- package/docs/workflows.md +64 -24
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +4 -3
- package/src/shared/data/language-corpora.json +32 -0
- package/src/shared/references/pipeline-contracts.md +6 -3
- package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
- 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-derive-assembly-shape.py +107 -0
- package/src/shared/scripts/skf-detect-docs.py +240 -4
- package/src/shared/scripts/skf-detect-language.py +82 -3
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-extract-public-api.py +53 -0
- 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-language-corpora.py +100 -0
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
- package/src/shared/scripts/skf-preapply.py +1 -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-shape-detect.py +576 -25
- 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 +41 -6
- 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 +3 -3
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/shared/scripts/skf-write-skill-brief.py +21 -5
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- 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 +211 -197
- package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
- 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/assets/skill-brief-schema.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +13 -18
- package/src/skf-brief-skill/references/confirm-brief.md +2 -15
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +27 -24
- 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 +6 -15
- package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
- package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +19 -13
- package/src/skf-campaign/SKILL.md +121 -0
- package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
- package/src/skf-campaign/customize.toml +75 -0
- package/src/skf-campaign/manifest.yaml +15 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
- package/src/skf-campaign/references/health-check.md +35 -0
- package/src/skf-campaign/references/step-01-setup.md +121 -0
- package/src/skf-campaign/references/step-02-strategy.md +102 -0
- package/src/skf-campaign/references/step-03-pins.md +56 -0
- package/src/skf-campaign/references/step-04-provenance.md +63 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
- package/src/skf-campaign/references/step-06-batch.md +88 -0
- package/src/skf-campaign/references/step-07-capstone.md +63 -0
- package/src/skf-campaign/references/step-08-verify.md +75 -0
- package/src/skf-campaign/references/step-09-refine.md +83 -0
- package/src/skf-campaign/references/step-10-export.md +106 -0
- package/src/skf-campaign/references/step-11-maintenance.md +84 -0
- package/src/skf-campaign/references/step-resume.md +115 -0
- package/src/skf-campaign/scripts/.gitkeep +0 -0
- package/src/skf-campaign/scripts/campaign-deps.py +244 -0
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
- package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
- package/src/skf-campaign/scripts/campaign-report.py +249 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
- package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
- package/src/skf-campaign/templates/campaign-report-template.md +54 -0
- package/src/skf-campaign/templates/kickoff-template.md +48 -0
- package/src/skf-create-skill/SKILL.md +15 -4
- package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
- 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 +37 -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 +20 -27
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +10 -8
- package/src/skf-create-skill/references/generate-artifacts.md +20 -25
- package/src/skf-create-skill/references/health-check.md +2 -2
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +27 -14
- package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +33 -15
- package/src/skf-create-skill/references/step-doc-sources.md +13 -4
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
- 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 +69 -29
- 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 +67 -36
- 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 +44 -10
- package/src/skf-drop-skill/references/select.md +93 -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 +32 -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 +16 -8
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +10 -8
- 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 +14 -11
- 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 +112 -82
- 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 +21 -13
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +11 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +10 -8
- package/src/skf-setup/references/report.md +26 -32
- 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 +111 -67
- 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 +39 -21
- 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 +15 -5
- 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 +135 -4
- 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 +11 -6
- package/src/skf-update-skill/references/detect-changes.md +48 -36
- package/src/skf-update-skill/references/health-check.md +5 -11
- package/src/skf-update-skill/references/init.md +40 -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 +27 -25
- 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/tools/cli/lib/ui.js +3 -2
- package/docs/deepwiki.md +0 -89
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF Render Metadata Stats — derive metadata.json's computed stats block and
|
|
7
|
+
confidence_distribution deterministically from the staged provenance-map.
|
|
8
|
+
|
|
9
|
+
Two places in `skf-create-skill` do the same pure arithmetic over the staged
|
|
10
|
+
staging JSON, by hand, every run:
|
|
11
|
+
|
|
12
|
+
1. **compile.md §4 (Build metadata.json)** bins each provenance entry by its
|
|
13
|
+
`signature_source` tier into `confidence_distribution.{t1,t1_low,t2,t3}`,
|
|
14
|
+
counts documented exports, and computes the coverage ratios. This is the
|
|
15
|
+
step that documents the LLM's recurring miscount — binning ~8 T2
|
|
16
|
+
annotations + ~80 T3 doc items *on top of* 59 exports and getting a
|
|
17
|
+
distribution that sums to 147 ≠ 59.
|
|
18
|
+
|
|
19
|
+
2. **validate.md §7 (Validate metadata.json)** re-derives the same sums,
|
|
20
|
+
ratios, and array-length counts to auto-fix any drift ("these are
|
|
21
|
+
computed values").
|
|
22
|
+
|
|
23
|
+
Both are pure projections over the provenance-map plus a small judgment
|
|
24
|
+
payload — the values the prompt genuinely decides (which count feeds
|
|
25
|
+
`exports_public_api`, the gated `effective_denominator`). This script owns the
|
|
26
|
+
arithmetic so the prompt stops doing it by hand; the judgment stays in the
|
|
27
|
+
prompt and is passed in.
|
|
28
|
+
|
|
29
|
+
Because the distribution is a `Counter` over `entries[]` — one entry counted
|
|
30
|
+
once by its tier — the script **cannot** reproduce the 147≠59 miscount: the
|
|
31
|
+
four bins always sum to the number of entries with a recognized
|
|
32
|
+
`signature_source`, never to an enrichment total.
|
|
33
|
+
|
|
34
|
+
Shape carve-outs (mirroring `skf-test-skill` coverage-check §4b): the
|
|
35
|
+
distribution ALWAYS bins `entries[]`, so it always sums to `len(entries)`. What
|
|
36
|
+
`exports_documented` equals is what differs by shape:
|
|
37
|
+
|
|
38
|
+
- **library** — `exports_documented = len(entries)`; the four bins sum to it.
|
|
39
|
+
- **reference-app** — `entries[]` are per-citation, so the distribution sums
|
|
40
|
+
to the *citation count* (= `len(entries)`), NOT to `exports_documented`
|
|
41
|
+
(which is the pattern-surface proxy `pattern_surfaces_documented`, a
|
|
42
|
+
judgment value). `effective_denominator` is never emitted.
|
|
43
|
+
- **stack** — `entries[]` are the cited constituent contracts, so the
|
|
44
|
+
distribution sums to the *constituent count* (= `len(entries)`), NOT to
|
|
45
|
+
`exports_documented` (a stack's own barrel is empty by design).
|
|
46
|
+
|
|
47
|
+
Modes
|
|
48
|
+
-----
|
|
49
|
+
|
|
50
|
+
Default (compute) — read the provenance-map path + a judgment payload on stdin;
|
|
51
|
+
emit the derived `stats` + `confidence_distribution` + a `coherence` report.
|
|
52
|
+
`compile.md §4` writes the returned values verbatim.
|
|
53
|
+
|
|
54
|
+
`--check <metadata.json>` — read the provenance-map path + the on-disk
|
|
55
|
+
metadata.json; re-derive the computed values (taking the judgment values —
|
|
56
|
+
`exports_public_api`, `exports_internal`, `effective_denominator` — from the
|
|
57
|
+
on-disk metadata itself) and compare against what is on disk. `coherence.ok`
|
|
58
|
+
is false with a `violations[]` list when the on-disk file drifted; the emitted
|
|
59
|
+
`stats`/`confidence_distribution` are the corrected values `validate.md §7`
|
|
60
|
+
writes back.
|
|
61
|
+
|
|
62
|
+
Judgment payload (stdin JSON, compute mode)::
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"exports_public_api": 40, # int — public-entry-point export count (judgment)
|
|
66
|
+
"exports_internal": 19, # int — non-underscore internal export count (judgment)
|
|
67
|
+
"effective_denominator": 12, # int|null — optional, stratified-scope monorepo only
|
|
68
|
+
"pattern_surfaces_documented": 8, # int — reference-app only (exports_documented proxy)
|
|
69
|
+
"exports_documented": 0, # int — stack only (own-barrel count, usually 0)
|
|
70
|
+
"scripts": [ ... ], # list — scripts[] array (or "scripts_count": int)
|
|
71
|
+
"assets": [ ... ] # list — assets[] array (or "assets_count": int)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Output JSON (both modes)::
|
|
75
|
+
|
|
76
|
+
{
|
|
77
|
+
"stats": {
|
|
78
|
+
"exports_documented": 59,
|
|
79
|
+
"exports_public_api": 40,
|
|
80
|
+
"exports_internal": 19,
|
|
81
|
+
"exports_total": 59,
|
|
82
|
+
"public_api_coverage": 1.475, # documented/public_api, null if public_api == 0
|
|
83
|
+
"total_coverage": 1.0, # documented/total, null if total == 0
|
|
84
|
+
"scripts_count": 0,
|
|
85
|
+
"assets_count": 0
|
|
86
|
+
},
|
|
87
|
+
"confidence_distribution": {"t1": 40, "t1_low": 5, "t2": 10, "t3": 4},
|
|
88
|
+
"coherence": {"ok": true, "violations": []}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Exit codes:
|
|
92
|
+
0 — coherence ok (no violations)
|
|
93
|
+
1 — coherence violations present (drift to auto-fix / bad signature_source);
|
|
94
|
+
the JSON on stdout is still authoritative — rely on it, not the code
|
|
95
|
+
2 — usage / IO / JSON-decode error
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
from __future__ import annotations
|
|
99
|
+
|
|
100
|
+
import argparse
|
|
101
|
+
import json
|
|
102
|
+
import sys
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Canonical signature_source tiers → distribution bin keys. Compared
|
|
107
|
+
# case-insensitively so "T1-low" / "t1-low" both land in t1_low.
|
|
108
|
+
_SIG_MAP = {"T1": "t1", "T1-LOW": "t1_low", "T2": "t2", "T3": "t3"}
|
|
109
|
+
_VALID_SHAPES = ("library", "stack", "reference-app")
|
|
110
|
+
_DIST_KEYS = ("t1", "t1_low", "t2", "t3")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --------------------------------------------------------------------------
|
|
114
|
+
# IO
|
|
115
|
+
# --------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_json(path: Path) -> object:
|
|
119
|
+
"""Read + parse a JSON file. Raises ValueError on any failure."""
|
|
120
|
+
try:
|
|
121
|
+
text = path.read_text(encoding="utf-8")
|
|
122
|
+
except OSError as exc:
|
|
123
|
+
raise ValueError(f"failed to read {path}: {exc}") from exc
|
|
124
|
+
try:
|
|
125
|
+
return json.loads(text)
|
|
126
|
+
except json.JSONDecodeError as exc:
|
|
127
|
+
raise ValueError(f"malformed JSON in {path}: {exc}") from exc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------
|
|
131
|
+
# Pure derivations
|
|
132
|
+
# --------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def bin_signature_sources(entries: list) -> tuple[dict[str, int], int]:
|
|
136
|
+
"""Counter over provenance entries by signature_source tier.
|
|
137
|
+
|
|
138
|
+
Returns (distribution, unrecognized) where distribution has the four
|
|
139
|
+
canonical bin keys and `unrecognized` counts entries whose
|
|
140
|
+
`signature_source` is missing or not one of T1/T1-low/T2/T3. Counting
|
|
141
|
+
entries (each exactly once, by its own tier) is what makes the 147≠59
|
|
142
|
+
enrichment-fold miscount structurally impossible.
|
|
143
|
+
"""
|
|
144
|
+
dist = {k: 0 for k in _DIST_KEYS}
|
|
145
|
+
unrecognized = 0
|
|
146
|
+
if not isinstance(entries, list):
|
|
147
|
+
return dist, unrecognized
|
|
148
|
+
for entry in entries:
|
|
149
|
+
raw = entry.get("signature_source") if isinstance(entry, dict) else None
|
|
150
|
+
key = _SIG_MAP.get(raw.strip().upper()) if isinstance(raw, str) else None
|
|
151
|
+
if key is not None:
|
|
152
|
+
dist[key] += 1
|
|
153
|
+
else:
|
|
154
|
+
unrecognized += 1
|
|
155
|
+
return dist, unrecognized
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ratio(numerator: int, denominator: int) -> float | None:
|
|
159
|
+
"""Coverage ratio; None when the denominator is 0 (per the spec's
|
|
160
|
+
null-when-empty rule). Rounded to 4 dp for deterministic output."""
|
|
161
|
+
if denominator == 0:
|
|
162
|
+
return None
|
|
163
|
+
return round(numerator / denominator, 4)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _int(value: object, default: int = 0) -> int:
|
|
167
|
+
"""Coerce a judgment value to int, tolerating None / bad types."""
|
|
168
|
+
if isinstance(value, bool):
|
|
169
|
+
return int(value)
|
|
170
|
+
if isinstance(value, (int, float)):
|
|
171
|
+
return int(value)
|
|
172
|
+
if isinstance(value, str):
|
|
173
|
+
try:
|
|
174
|
+
return int(value.strip())
|
|
175
|
+
except ValueError:
|
|
176
|
+
return default
|
|
177
|
+
return default
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _count(judgment: dict, arr_key: str, count_key: str) -> int:
|
|
181
|
+
"""Length of an array field if present, else the scalar count field."""
|
|
182
|
+
arr = judgment.get(arr_key)
|
|
183
|
+
if isinstance(arr, list):
|
|
184
|
+
return len(arr)
|
|
185
|
+
return _int(judgment.get(count_key), 0)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def count_file_entries_by_type(prov: dict) -> dict[str, int]:
|
|
189
|
+
"""Count provenance-map file_entries[] by file_type."""
|
|
190
|
+
counts = {"script": 0, "asset": 0, "doc": 0, "other": 0}
|
|
191
|
+
fe = prov.get("file_entries")
|
|
192
|
+
if isinstance(fe, list):
|
|
193
|
+
for row in fe:
|
|
194
|
+
if not isinstance(row, dict):
|
|
195
|
+
counts["other"] += 1
|
|
196
|
+
continue
|
|
197
|
+
ft = row.get("file_type")
|
|
198
|
+
counts[ft if ft in ("script", "asset", "doc") else "other"] += 1
|
|
199
|
+
return counts
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def derive_stats(prov: dict, judgment: dict, shape: str = "library") -> dict:
|
|
203
|
+
"""Derive the stats block + confidence_distribution for one skill.
|
|
204
|
+
|
|
205
|
+
`prov` is the parsed provenance-map. `judgment` carries the values the
|
|
206
|
+
prompt decides. `shape` selects how `exports_documented` is set. The
|
|
207
|
+
distribution always bins `entries[]`.
|
|
208
|
+
"""
|
|
209
|
+
entries = prov.get("entries")
|
|
210
|
+
entries = entries if isinstance(entries, list) else []
|
|
211
|
+
dist, unrecognized = bin_signature_sources(entries)
|
|
212
|
+
entry_count = len(entries)
|
|
213
|
+
|
|
214
|
+
if shape == "reference-app":
|
|
215
|
+
exports_documented = _int(judgment.get("pattern_surfaces_documented"), 0)
|
|
216
|
+
elif shape == "stack":
|
|
217
|
+
exports_documented = _int(judgment.get("exports_documented"), 0)
|
|
218
|
+
else: # library
|
|
219
|
+
exports_documented = entry_count
|
|
220
|
+
|
|
221
|
+
exports_public_api = _int(judgment.get("exports_public_api"), 0)
|
|
222
|
+
exports_internal = _int(judgment.get("exports_internal"), 0)
|
|
223
|
+
exports_total = exports_public_api + exports_internal
|
|
224
|
+
|
|
225
|
+
stats: dict[str, object] = {
|
|
226
|
+
"exports_documented": exports_documented,
|
|
227
|
+
"exports_public_api": exports_public_api,
|
|
228
|
+
"exports_internal": exports_internal,
|
|
229
|
+
"exports_total": exports_total,
|
|
230
|
+
"public_api_coverage": ratio(exports_documented, exports_public_api),
|
|
231
|
+
"total_coverage": ratio(exports_documented, exports_total),
|
|
232
|
+
"scripts_count": _count(judgment, "scripts", "scripts_count"),
|
|
233
|
+
"assets_count": _count(judgment, "assets", "assets_count"),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# reference-app: emit the pattern-surface proxy alongside exports_documented
|
|
237
|
+
if shape == "reference-app":
|
|
238
|
+
stats["pattern_surfaces_documented"] = exports_documented
|
|
239
|
+
|
|
240
|
+
# effective_denominator: optional, and never for a reference app (its
|
|
241
|
+
# coverage basis is pattern surfaces, not an export barrel).
|
|
242
|
+
eff = judgment.get("effective_denominator")
|
|
243
|
+
if eff is not None and shape != "reference-app":
|
|
244
|
+
stats["effective_denominator"] = _int(eff, 0)
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
"stats": stats,
|
|
248
|
+
"confidence_distribution": dist,
|
|
249
|
+
"_derivation": {
|
|
250
|
+
"shape": shape,
|
|
251
|
+
"entry_count": entry_count,
|
|
252
|
+
"distribution_sum": sum(dist.values()),
|
|
253
|
+
"unrecognized_signature_source": unrecognized,
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# --------------------------------------------------------------------------
|
|
259
|
+
# Coherence
|
|
260
|
+
# --------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cmp_int(violations: list, field: str, expected, actual, note: str | None = None) -> None:
|
|
264
|
+
if expected != actual:
|
|
265
|
+
v = {"field": field, "expected": expected, "actual": actual}
|
|
266
|
+
if note:
|
|
267
|
+
v["note"] = note
|
|
268
|
+
violations.append(v)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _cmp_ratio(violations: list, field: str, expected, actual) -> None:
|
|
272
|
+
norm = round(float(actual), 4) if isinstance(actual, (int, float)) and not isinstance(actual, bool) else actual
|
|
273
|
+
exp = round(expected, 4) if isinstance(expected, float) else expected
|
|
274
|
+
if exp != norm:
|
|
275
|
+
violations.append({"field": field, "expected": expected, "actual": actual})
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def coherence_compute(derived: dict, prov: dict) -> dict:
|
|
279
|
+
"""Internal-consistency checks for compute mode.
|
|
280
|
+
|
|
281
|
+
The distribution must account for every provenance entry (a bin-sum below
|
|
282
|
+
the entry count means some entries carry a missing/unrecognized
|
|
283
|
+
signature_source). When the provenance-map already tracks scripts/assets in
|
|
284
|
+
file_entries[], those counts must agree with the stats counts.
|
|
285
|
+
"""
|
|
286
|
+
violations: list[dict] = []
|
|
287
|
+
entry_count = derived["_derivation"]["entry_count"]
|
|
288
|
+
dist_sum = derived["_derivation"]["distribution_sum"]
|
|
289
|
+
if dist_sum != entry_count:
|
|
290
|
+
missing = entry_count - dist_sum
|
|
291
|
+
violations.append({
|
|
292
|
+
"field": "confidence_distribution",
|
|
293
|
+
"expected": entry_count,
|
|
294
|
+
"actual": dist_sum,
|
|
295
|
+
"note": (
|
|
296
|
+
f"{missing} provenance entr{'y' if abs(missing) == 1 else 'ies'} "
|
|
297
|
+
"with missing or unrecognized signature_source"
|
|
298
|
+
),
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
fe = count_file_entries_by_type(prov)
|
|
302
|
+
sc = derived["stats"]["scripts_count"]
|
|
303
|
+
ac = derived["stats"]["assets_count"]
|
|
304
|
+
# Conservative: only cross-check a type once the provenance-map actually
|
|
305
|
+
# carries rows of that type (file_entries are written in a later compile
|
|
306
|
+
# section, so an empty count is "not yet populated", not "zero").
|
|
307
|
+
if fe["script"] > 0:
|
|
308
|
+
_cmp_int(violations, "stats.scripts_count", fe["script"], sc,
|
|
309
|
+
"stats.scripts_count disagrees with provenance-map file_entries[file_type=script]")
|
|
310
|
+
if fe["asset"] > 0:
|
|
311
|
+
_cmp_int(violations, "stats.assets_count", fe["asset"], ac,
|
|
312
|
+
"stats.assets_count disagrees with provenance-map file_entries[file_type=asset]")
|
|
313
|
+
|
|
314
|
+
return {"ok": not violations, "violations": violations}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def check_stats(derived: dict, metadata: dict, prov: dict) -> dict:
|
|
318
|
+
"""Compare re-derived values against the on-disk metadata.json.
|
|
319
|
+
|
|
320
|
+
Every mismatch is a `{field, expected, actual}` violation, where `expected`
|
|
321
|
+
is the re-derived (correct) value and `actual` is what is on disk.
|
|
322
|
+
validate.md §7 either writes `derived` verbatim or fixes each violation.
|
|
323
|
+
"""
|
|
324
|
+
violations: list[dict] = []
|
|
325
|
+
m_stats = metadata.get("stats") if isinstance(metadata.get("stats"), dict) else {}
|
|
326
|
+
d_stats = derived["stats"]
|
|
327
|
+
|
|
328
|
+
_cmp_int(violations, "stats.exports_documented", d_stats["exports_documented"],
|
|
329
|
+
m_stats.get("exports_documented"))
|
|
330
|
+
_cmp_int(violations, "stats.exports_total", d_stats["exports_total"],
|
|
331
|
+
m_stats.get("exports_total"))
|
|
332
|
+
_cmp_ratio(violations, "stats.public_api_coverage", d_stats["public_api_coverage"],
|
|
333
|
+
m_stats.get("public_api_coverage"))
|
|
334
|
+
_cmp_ratio(violations, "stats.total_coverage", d_stats["total_coverage"],
|
|
335
|
+
m_stats.get("total_coverage"))
|
|
336
|
+
|
|
337
|
+
m_dist = metadata.get("confidence_distribution") if isinstance(metadata.get("confidence_distribution"), dict) else {}
|
|
338
|
+
for key in _DIST_KEYS:
|
|
339
|
+
_cmp_int(violations, f"confidence_distribution.{key}",
|
|
340
|
+
derived["confidence_distribution"][key], m_dist.get(key))
|
|
341
|
+
|
|
342
|
+
# array length vs its stats count (validate.md §7: "verify
|
|
343
|
+
# stats.scripts_count/stats.assets_count match array lengths")
|
|
344
|
+
scripts_arr = metadata.get("scripts")
|
|
345
|
+
if isinstance(scripts_arr, list):
|
|
346
|
+
_cmp_int(violations, "stats.scripts_count", len(scripts_arr),
|
|
347
|
+
m_stats.get("scripts_count"),
|
|
348
|
+
"stats.scripts_count does not match len(scripts[])")
|
|
349
|
+
assets_arr = metadata.get("assets")
|
|
350
|
+
if isinstance(assets_arr, list):
|
|
351
|
+
_cmp_int(violations, "stats.assets_count", len(assets_arr),
|
|
352
|
+
m_stats.get("assets_count"),
|
|
353
|
+
"stats.assets_count does not match len(assets[])")
|
|
354
|
+
|
|
355
|
+
# provenance-map file_entries count vs metadata scripts/assets counts
|
|
356
|
+
if isinstance(scripts_arr, list) or isinstance(assets_arr, list):
|
|
357
|
+
fe = count_file_entries_by_type(prov)
|
|
358
|
+
claimed_sc = _int(m_stats.get("scripts_count"), 0)
|
|
359
|
+
claimed_ac = _int(m_stats.get("assets_count"), 0)
|
|
360
|
+
if fe["script"] != claimed_sc and (fe["script"] > 0 or claimed_sc > 0):
|
|
361
|
+
_cmp_int(violations, "provenance.file_entries.script", claimed_sc, fe["script"],
|
|
362
|
+
"provenance-map file_entries[file_type=script] count disagrees with metadata scripts_count")
|
|
363
|
+
if fe["asset"] != claimed_ac and (fe["asset"] > 0 or claimed_ac > 0):
|
|
364
|
+
_cmp_int(violations, "provenance.file_entries.asset", claimed_ac, fe["asset"],
|
|
365
|
+
"provenance-map file_entries[file_type=asset] count disagrees with metadata assets_count")
|
|
366
|
+
|
|
367
|
+
return {"ok": not violations, "violations": violations}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def detect_shape(metadata: dict) -> str:
|
|
371
|
+
"""Infer the shape from an on-disk metadata.json (used by --check when
|
|
372
|
+
--shape is not supplied)."""
|
|
373
|
+
if metadata.get("scope_type") == "reference-app":
|
|
374
|
+
return "reference-app"
|
|
375
|
+
if metadata.get("skill_type") == "stack":
|
|
376
|
+
return "stack"
|
|
377
|
+
return "library"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# --------------------------------------------------------------------------
|
|
381
|
+
# CLI
|
|
382
|
+
# --------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _read_stdin_judgment() -> dict:
|
|
386
|
+
raw = sys.stdin.read() if not sys.stdin.isatty() else ""
|
|
387
|
+
if not raw.strip():
|
|
388
|
+
return {}
|
|
389
|
+
payload = json.loads(raw)
|
|
390
|
+
if not isinstance(payload, dict):
|
|
391
|
+
raise ValueError("stdin judgment payload must be a JSON object")
|
|
392
|
+
return payload
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _emit(result: dict, out_path: str | None) -> None:
|
|
396
|
+
text = json.dumps(result, indent=2)
|
|
397
|
+
if out_path:
|
|
398
|
+
Path(out_path).write_text(text + "\n", encoding="utf-8")
|
|
399
|
+
else:
|
|
400
|
+
sys.stdout.write(text + "\n")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
404
|
+
parser = argparse.ArgumentParser(
|
|
405
|
+
prog="skf-render-metadata-stats",
|
|
406
|
+
description=(
|
|
407
|
+
"Derive metadata.json stats + confidence_distribution from the staged "
|
|
408
|
+
"provenance-map (compute mode), or re-verify an on-disk metadata.json "
|
|
409
|
+
"against the map (--check). Replaces the hand-binning/summing in "
|
|
410
|
+
"skf-create-skill compile.md §4 and validate.md §7."
|
|
411
|
+
),
|
|
412
|
+
)
|
|
413
|
+
parser.add_argument("provenance_map", help="path to the staged provenance-map.json")
|
|
414
|
+
parser.add_argument(
|
|
415
|
+
"--check", metavar="METADATA_JSON", dest="check",
|
|
416
|
+
help="verify this on-disk metadata.json against the provenance-map instead of "
|
|
417
|
+
"reading judgment from stdin; emits corrected stats + a violations list",
|
|
418
|
+
)
|
|
419
|
+
parser.add_argument(
|
|
420
|
+
"--shape", choices=_VALID_SHAPES, default=None,
|
|
421
|
+
help="assembly shape (default: library in compute mode; inferred from "
|
|
422
|
+
"metadata.json in --check mode). Selects what exports_documented equals.",
|
|
423
|
+
)
|
|
424
|
+
parser.add_argument("-o", "--output", dest="output", help="write JSON here (default: stdout)")
|
|
425
|
+
parser.add_argument("--verbose", action="store_true", help="diagnostics to stderr")
|
|
426
|
+
return parser
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def main(argv: list[str] | None = None) -> int:
|
|
430
|
+
args = build_parser().parse_args(argv)
|
|
431
|
+
|
|
432
|
+
prov_path = Path(args.provenance_map)
|
|
433
|
+
if not prov_path.is_file():
|
|
434
|
+
sys.stderr.write(f"error: provenance-map not found: {prov_path}\n")
|
|
435
|
+
return 2
|
|
436
|
+
try:
|
|
437
|
+
prov = load_json(prov_path)
|
|
438
|
+
except ValueError as exc:
|
|
439
|
+
sys.stderr.write(f"error: {exc}\n")
|
|
440
|
+
return 2
|
|
441
|
+
if not isinstance(prov, dict):
|
|
442
|
+
sys.stderr.write("error: provenance-map must be a JSON object at top level\n")
|
|
443
|
+
return 2
|
|
444
|
+
|
|
445
|
+
if args.check:
|
|
446
|
+
meta_path = Path(args.check)
|
|
447
|
+
if not meta_path.is_file():
|
|
448
|
+
sys.stderr.write(f"error: metadata.json not found: {meta_path}\n")
|
|
449
|
+
return 2
|
|
450
|
+
try:
|
|
451
|
+
metadata = load_json(meta_path)
|
|
452
|
+
except ValueError as exc:
|
|
453
|
+
sys.stderr.write(f"error: {exc}\n")
|
|
454
|
+
return 2
|
|
455
|
+
if not isinstance(metadata, dict):
|
|
456
|
+
sys.stderr.write("error: metadata.json must be a JSON object at top level\n")
|
|
457
|
+
return 2
|
|
458
|
+
shape = args.shape or detect_shape(metadata)
|
|
459
|
+
m_stats = metadata.get("stats") if isinstance(metadata.get("stats"), dict) else {}
|
|
460
|
+
judgment = {
|
|
461
|
+
"exports_public_api": m_stats.get("exports_public_api"),
|
|
462
|
+
"exports_internal": m_stats.get("exports_internal"),
|
|
463
|
+
"effective_denominator": m_stats.get("effective_denominator"),
|
|
464
|
+
"pattern_surfaces_documented": m_stats.get("pattern_surfaces_documented"),
|
|
465
|
+
"exports_documented": m_stats.get("exports_documented"),
|
|
466
|
+
"scripts": metadata.get("scripts"),
|
|
467
|
+
"assets": metadata.get("assets"),
|
|
468
|
+
}
|
|
469
|
+
derived = derive_stats(prov, judgment, shape)
|
|
470
|
+
coherence = check_stats(derived, metadata, prov)
|
|
471
|
+
else:
|
|
472
|
+
try:
|
|
473
|
+
judgment = _read_stdin_judgment()
|
|
474
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
475
|
+
sys.stderr.write(f"error: {exc}\n")
|
|
476
|
+
return 2
|
|
477
|
+
shape = args.shape or "library"
|
|
478
|
+
derived = derive_stats(prov, judgment, shape)
|
|
479
|
+
coherence = coherence_compute(derived, prov)
|
|
480
|
+
|
|
481
|
+
if args.verbose:
|
|
482
|
+
sys.stderr.write(
|
|
483
|
+
f"shape={shape} entries={derived['_derivation']['entry_count']} "
|
|
484
|
+
f"dist_sum={derived['_derivation']['distribution_sum']} "
|
|
485
|
+
f"unrecognized={derived['_derivation']['unrecognized_signature_source']} "
|
|
486
|
+
f"violations={len(coherence['violations'])}\n"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
result = {
|
|
490
|
+
"stats": derived["stats"],
|
|
491
|
+
"confidence_distribution": derived["confidence_distribution"],
|
|
492
|
+
"coherence": coherence,
|
|
493
|
+
}
|
|
494
|
+
_emit(result, args.output)
|
|
495
|
+
return 0 if coherence["ok"] else 1
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
if __name__ == "__main__":
|
|
499
|
+
raise SystemExit(main())
|