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,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Deterministic technology-coverage tally for skf-verify-stack (coverage.md §6).
|
|
7
|
+
|
|
8
|
+
The coverage verdicts themselves (Covered / Missing / Replaced) are LLM judgment
|
|
9
|
+
— matching an architecture technology to a generated skill involves aliases and
|
|
10
|
+
prose. This script takes that already-decided coverage matrix and does only the
|
|
11
|
+
part with one correct answer: counting each verdict class and turning the counts
|
|
12
|
+
into `coveragePercentage`.
|
|
13
|
+
|
|
14
|
+
Two gotchas make the in-prose version drift between runs, so they live here
|
|
15
|
+
instead:
|
|
16
|
+
|
|
17
|
+
* The denominator excludes Replaced. `live_count = Covered + Missing`;
|
|
18
|
+
technologies flagged Replaced (intentionally being removed) are not a gap and
|
|
19
|
+
must not dilute the percentage.
|
|
20
|
+
* Rounding is pinned to half-up to the nearest integer, so the same matrix
|
|
21
|
+
always yields the same `coveragePercentage` (the shared schema declares
|
|
22
|
+
`coveragePercentage: <0..100 integer>` but not the rounding rule).
|
|
23
|
+
|
|
24
|
+
CLI usage:
|
|
25
|
+
uv run skf-coverage-tally.py '<JSON>' # JSON literal positional
|
|
26
|
+
uv run skf-coverage-tally.py --json-input '<JSON>' # explicit flag form
|
|
27
|
+
cat input.json | uv run skf-coverage-tally.py --stdin # piped input
|
|
28
|
+
|
|
29
|
+
Input schema (one object):
|
|
30
|
+
{
|
|
31
|
+
"rows": [
|
|
32
|
+
{"technology": "react", "verdict": "Covered"},
|
|
33
|
+
{"technology": "postgres","verdict": "Missing"},
|
|
34
|
+
{"technology": "old-orm", "verdict": "Replaced"}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Output (stdout, one object):
|
|
39
|
+
{
|
|
40
|
+
"covered_count": <int>,
|
|
41
|
+
"missing_count": <int>,
|
|
42
|
+
"replaced_count": <int>,
|
|
43
|
+
"live_count": <int>, # Covered + Missing (denominator)
|
|
44
|
+
"total_referenced": <int>, # all rows after dedup
|
|
45
|
+
"coverage_percentage": <int> # round-half-up(covered/live*100); 0 when live==0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Exit codes:
|
|
49
|
+
0 — tally emitted successfully
|
|
50
|
+
1 — no input / input could not be parsed as JSON
|
|
51
|
+
2 — input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from __future__ import annotations
|
|
55
|
+
|
|
56
|
+
import argparse
|
|
57
|
+
import json
|
|
58
|
+
import math
|
|
59
|
+
import sys
|
|
60
|
+
|
|
61
|
+
VALID_VERDICTS = ("Covered", "Missing", "Replaced")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def make_error(message):
|
|
65
|
+
return {"error": message, "code": "INVALID_INPUT"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate(inp):
|
|
69
|
+
if inp is None or not isinstance(inp, dict):
|
|
70
|
+
return "Input must be a JSON object"
|
|
71
|
+
rows = inp.get("rows")
|
|
72
|
+
if not isinstance(rows, list):
|
|
73
|
+
return "Missing or invalid required field: rows (must be a list)"
|
|
74
|
+
seen = set()
|
|
75
|
+
for i, row in enumerate(rows):
|
|
76
|
+
if not isinstance(row, dict):
|
|
77
|
+
return f"rows[{i}] must be an object"
|
|
78
|
+
tech = row.get("technology")
|
|
79
|
+
if not isinstance(tech, str) or not tech.strip():
|
|
80
|
+
return f"rows[{i}] requires a non-empty string `technology`"
|
|
81
|
+
verdict = row.get("verdict")
|
|
82
|
+
if verdict not in VALID_VERDICTS:
|
|
83
|
+
return (
|
|
84
|
+
f"rows[{i}] verdict {verdict!r} is not one of: "
|
|
85
|
+
f"{', '.join(VALID_VERDICTS)}"
|
|
86
|
+
)
|
|
87
|
+
norm = tech.strip().lower()
|
|
88
|
+
if norm in seen:
|
|
89
|
+
return f"duplicate technology {tech!r} — the coverage list must be deduplicated first"
|
|
90
|
+
seen.add(norm)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def tally(inp):
|
|
95
|
+
"""Pure tally. Counts each verdict class and computes coverage_percentage."""
|
|
96
|
+
err = _validate(inp)
|
|
97
|
+
if err:
|
|
98
|
+
return make_error(err)
|
|
99
|
+
|
|
100
|
+
covered = missing = replaced = 0
|
|
101
|
+
for row in inp["rows"]:
|
|
102
|
+
verdict = row["verdict"]
|
|
103
|
+
if verdict == "Covered":
|
|
104
|
+
covered += 1
|
|
105
|
+
elif verdict == "Missing":
|
|
106
|
+
missing += 1
|
|
107
|
+
else: # Replaced
|
|
108
|
+
replaced += 1
|
|
109
|
+
|
|
110
|
+
live = covered + missing
|
|
111
|
+
percentage = math.floor(covered / live * 100 + 0.5) if live > 0 else 0
|
|
112
|
+
return {
|
|
113
|
+
"covered_count": covered,
|
|
114
|
+
"missing_count": missing,
|
|
115
|
+
"replaced_count": replaced,
|
|
116
|
+
"live_count": live,
|
|
117
|
+
"total_referenced": covered + missing + replaced,
|
|
118
|
+
"coverage_percentage": percentage,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- CLI --------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_parser():
|
|
126
|
+
parser = argparse.ArgumentParser(
|
|
127
|
+
prog="skf-coverage-tally",
|
|
128
|
+
description=(
|
|
129
|
+
"Deterministic technology-coverage tally (coverage.md §6). Counts "
|
|
130
|
+
"Covered/Missing/Replaced verdicts and computes coveragePercentage "
|
|
131
|
+
"with Replaced excluded from the denominator and half-up rounding."
|
|
132
|
+
),
|
|
133
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
134
|
+
epilog=(
|
|
135
|
+
"Example:\n"
|
|
136
|
+
" uv run skf-coverage-tally.py "
|
|
137
|
+
"'{\"rows\":[{\"technology\":\"react\",\"verdict\":\"Covered\"},"
|
|
138
|
+
"{\"technology\":\"postgres\",\"verdict\":\"Missing\"}]}'"
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
src = parser.add_mutually_exclusive_group()
|
|
142
|
+
src.add_argument(
|
|
143
|
+
"json_input",
|
|
144
|
+
nargs="?",
|
|
145
|
+
help="JSON object as a positional argument (single-quote it on the shell).",
|
|
146
|
+
)
|
|
147
|
+
src.add_argument(
|
|
148
|
+
"--json-input",
|
|
149
|
+
dest="json_input_flag",
|
|
150
|
+
help="JSON object passed via flag (overrides positional).",
|
|
151
|
+
)
|
|
152
|
+
src.add_argument(
|
|
153
|
+
"--stdin",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Read the JSON object from stdin.",
|
|
156
|
+
)
|
|
157
|
+
return parser
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _resolve_input(args):
|
|
161
|
+
if args.stdin:
|
|
162
|
+
return sys.stdin.read()
|
|
163
|
+
if args.json_input_flag is not None:
|
|
164
|
+
return args.json_input_flag
|
|
165
|
+
if args.json_input is not None:
|
|
166
|
+
return args.json_input
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def main(argv=None):
|
|
171
|
+
parser = _build_parser()
|
|
172
|
+
args = parser.parse_args(argv)
|
|
173
|
+
raw = _resolve_input(args)
|
|
174
|
+
if not raw.strip():
|
|
175
|
+
parser.print_usage(file=sys.stderr)
|
|
176
|
+
print(
|
|
177
|
+
"error: no input provided (positional arg, --json-input, or --stdin)",
|
|
178
|
+
file=sys.stderr,
|
|
179
|
+
)
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
data = json.loads(raw)
|
|
184
|
+
except json.JSONDecodeError as exc:
|
|
185
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
result = tally(data)
|
|
189
|
+
print(json.dumps(result, indent=2))
|
|
190
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
191
|
+
return 2
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""Deterministic previous-vs-current delta for skf-verify-stack (synthesize.md §3).
|
|
7
|
+
|
|
8
|
+
Reading two feasibility reports and deciding each finding's verdict is judgment;
|
|
9
|
+
classifying how the two runs differ is not. Given the two already-extracted
|
|
10
|
+
finding sets, "improved / regressed / new / unchanged" is a set-diff plus a
|
|
11
|
+
fixed verdict ranking — one correct answer per input. Doing it in prose lets the
|
|
12
|
+
counts drift (mismatched pair keys, inconsistent ranking), so it lives here.
|
|
13
|
+
|
|
14
|
+
Verdict ranking (higher = healthier):
|
|
15
|
+
* coverage — Missing(0) < Covered(1). Replaced is intentional removal, not a
|
|
16
|
+
gap; Replaced findings are bucketed separately and never scored improved/
|
|
17
|
+
regressed (mirrors their exclusion from the coverage denominator).
|
|
18
|
+
* integration — Blocked(0) < Risky(1) < Plausible(2) < Verified(3).
|
|
19
|
+
* confidence tier — T2(1) < T1-low(2) < T1(3); a drop is a regression.
|
|
20
|
+
|
|
21
|
+
Matching keys are normalized so identity is stable across runs:
|
|
22
|
+
* coverage findings match on technology.lower()
|
|
23
|
+
* integration findings match on the unordered pair {libA, libB} (lowercased),
|
|
24
|
+
so "A↔B" in one run equals "B↔A" in the other.
|
|
25
|
+
|
|
26
|
+
CLI usage:
|
|
27
|
+
uv run skf-report-delta.py '<JSON>' # JSON literal positional
|
|
28
|
+
uv run skf-report-delta.py --json-input '<JSON>' # explicit flag form
|
|
29
|
+
cat input.json | uv run skf-report-delta.py --stdin # piped input
|
|
30
|
+
|
|
31
|
+
Input schema (one object):
|
|
32
|
+
{
|
|
33
|
+
"previous": {
|
|
34
|
+
"coverage": [{"technology": "react", "verdict": "Covered"}, ...],
|
|
35
|
+
"integration": [{"libA": "react", "libB": "express", "verdict": "Verified"}, ...]
|
|
36
|
+
},
|
|
37
|
+
"current": { ... same shape ... },
|
|
38
|
+
"previousTiers": {"react": "T1", ...}, # optional, skill_name -> tier
|
|
39
|
+
"currentTiers": {"react": "T2", ...} # optional
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Output (stdout, one object):
|
|
43
|
+
{
|
|
44
|
+
"improved": [labels], "improvedCount": <int>,
|
|
45
|
+
"regressed": [labels], "regressedCount": <int>,
|
|
46
|
+
"unchanged": [labels], "unchangedCount": <int>,
|
|
47
|
+
"new": [labels], "newCount": <int>,
|
|
48
|
+
"dropped": [labels], "droppedCount": <int>,
|
|
49
|
+
"replaced": [labels], "replacedCount": <int>,
|
|
50
|
+
"tierDowngrades": [{"skill": "...", "from": "T1", "to": "T2"}],
|
|
51
|
+
"tierDowngradeCount": <int>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Exit codes:
|
|
55
|
+
0 — delta emitted successfully
|
|
56
|
+
1 — no input / input could not be parsed as JSON
|
|
57
|
+
2 — input parsed but schema/semantics invalid (error object emitted as JSON)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import argparse
|
|
63
|
+
import json
|
|
64
|
+
import sys
|
|
65
|
+
|
|
66
|
+
COVERAGE_RANK = {"Missing": 0, "Covered": 1}
|
|
67
|
+
COVERAGE_VERDICTS = ("Covered", "Missing", "Replaced")
|
|
68
|
+
INTEGRATION_RANK = {"Blocked": 0, "Risky": 1, "Plausible": 2, "Verified": 3}
|
|
69
|
+
INTEGRATION_VERDICTS = tuple(INTEGRATION_RANK)
|
|
70
|
+
TIER_RANK = {"T2": 1, "T1-LOW": 2, "T1": 3, "T3": 0}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_error(message):
|
|
74
|
+
return {"error": message, "code": "INVALID_INPUT"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validate_side(side, name):
|
|
78
|
+
if not isinstance(side, dict):
|
|
79
|
+
return f"`{name}` must be an object with `coverage`/`integration` lists"
|
|
80
|
+
cov = side.get("coverage", [])
|
|
81
|
+
integ = side.get("integration", [])
|
|
82
|
+
if not isinstance(cov, list) or not isinstance(integ, list):
|
|
83
|
+
return f"`{name}.coverage` and `{name}.integration` must be lists"
|
|
84
|
+
for i, row in enumerate(cov):
|
|
85
|
+
if not isinstance(row, dict) or not isinstance(row.get("technology"), str):
|
|
86
|
+
return f"`{name}.coverage[{i}]` requires a string `technology`"
|
|
87
|
+
if row.get("verdict") not in COVERAGE_VERDICTS:
|
|
88
|
+
return (
|
|
89
|
+
f"`{name}.coverage[{i}]` verdict {row.get('verdict')!r} not one of: "
|
|
90
|
+
f"{', '.join(COVERAGE_VERDICTS)}"
|
|
91
|
+
)
|
|
92
|
+
for i, row in enumerate(integ):
|
|
93
|
+
if not isinstance(row, dict) or not isinstance(row.get("libA"), str) or not isinstance(row.get("libB"), str):
|
|
94
|
+
return f"`{name}.integration[{i}]` requires string `libA` and `libB`"
|
|
95
|
+
if row.get("verdict") not in INTEGRATION_VERDICTS:
|
|
96
|
+
return (
|
|
97
|
+
f"`{name}.integration[{i}]` verdict {row.get('verdict')!r} not one of: "
|
|
98
|
+
f"{', '.join(INTEGRATION_VERDICTS)}"
|
|
99
|
+
)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _validate(inp):
|
|
104
|
+
if inp is None or not isinstance(inp, dict):
|
|
105
|
+
return "Input must be a JSON object"
|
|
106
|
+
if "previous" not in inp or "current" not in inp:
|
|
107
|
+
return "Input requires `previous` and `current` objects"
|
|
108
|
+
for name in ("previous", "current"):
|
|
109
|
+
err = _validate_side(inp[name], name)
|
|
110
|
+
if err:
|
|
111
|
+
return err
|
|
112
|
+
for key in ("previousTiers", "currentTiers"):
|
|
113
|
+
tiers = inp.get(key)
|
|
114
|
+
if tiers is not None and not isinstance(tiers, dict):
|
|
115
|
+
return f"`{key}` must be an object mapping skill_name -> tier"
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _index(side):
|
|
120
|
+
"""Return {normalized_key: (label, domain, verdict)} for one report side."""
|
|
121
|
+
out = {}
|
|
122
|
+
for row in side.get("coverage", []):
|
|
123
|
+
tech = row["technology"].strip()
|
|
124
|
+
out[("cov", tech.lower())] = (tech, "coverage", row["verdict"])
|
|
125
|
+
for row in side.get("integration", []):
|
|
126
|
+
a, b = row["libA"].strip(), row["libB"].strip()
|
|
127
|
+
pair = tuple(sorted([a.lower(), b.lower()]))
|
|
128
|
+
out[("int", pair)] = (f"{a} ↔ {b}", "integration", row["verdict"])
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _rank(domain, verdict):
|
|
133
|
+
return COVERAGE_RANK.get(verdict) if domain == "coverage" else INTEGRATION_RANK.get(verdict)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def compute(inp):
|
|
137
|
+
"""Pure delta over the two extracted finding sets."""
|
|
138
|
+
err = _validate(inp)
|
|
139
|
+
if err:
|
|
140
|
+
return make_error(err)
|
|
141
|
+
|
|
142
|
+
prev = _index(inp["previous"])
|
|
143
|
+
curr = _index(inp["current"])
|
|
144
|
+
|
|
145
|
+
buckets = {k: [] for k in ("improved", "regressed", "unchanged", "new", "dropped", "replaced")}
|
|
146
|
+
|
|
147
|
+
for key, (label, domain, verdict) in curr.items():
|
|
148
|
+
if key not in prev:
|
|
149
|
+
# Replaced-on-arrival is informational, not a regression/new gap.
|
|
150
|
+
if verdict == "Replaced":
|
|
151
|
+
buckets["replaced"].append(label)
|
|
152
|
+
else:
|
|
153
|
+
buckets["new"].append(label)
|
|
154
|
+
continue
|
|
155
|
+
_, _, prev_verdict = prev[key]
|
|
156
|
+
if verdict == "Replaced" or prev_verdict == "Replaced":
|
|
157
|
+
buckets["replaced"].append(label)
|
|
158
|
+
continue
|
|
159
|
+
pr, cr = _rank(domain, prev_verdict), _rank(domain, verdict)
|
|
160
|
+
if cr > pr:
|
|
161
|
+
buckets["improved"].append(label)
|
|
162
|
+
elif cr < pr:
|
|
163
|
+
buckets["regressed"].append(label)
|
|
164
|
+
else:
|
|
165
|
+
buckets["unchanged"].append(label)
|
|
166
|
+
|
|
167
|
+
for key, (label, _domain, verdict) in prev.items():
|
|
168
|
+
if key not in curr:
|
|
169
|
+
if verdict == "Replaced":
|
|
170
|
+
buckets["replaced"].append(label)
|
|
171
|
+
else:
|
|
172
|
+
buckets["dropped"].append(label)
|
|
173
|
+
|
|
174
|
+
tier_downgrades = []
|
|
175
|
+
prev_tiers = inp.get("previousTiers") or {}
|
|
176
|
+
curr_tiers = inp.get("currentTiers") or {}
|
|
177
|
+
for skill in sorted(set(prev_tiers) & set(curr_tiers)):
|
|
178
|
+
pr = TIER_RANK.get(str(prev_tiers[skill]).upper())
|
|
179
|
+
cr = TIER_RANK.get(str(curr_tiers[skill]).upper())
|
|
180
|
+
if pr is None or cr is None:
|
|
181
|
+
continue
|
|
182
|
+
if cr < pr:
|
|
183
|
+
tier_downgrades.append({"skill": skill, "from": prev_tiers[skill], "to": curr_tiers[skill]})
|
|
184
|
+
|
|
185
|
+
result = {}
|
|
186
|
+
for name, items in buckets.items():
|
|
187
|
+
result[name] = sorted(items)
|
|
188
|
+
result[name + "Count"] = len(items)
|
|
189
|
+
result["tierDowngrades"] = tier_downgrades
|
|
190
|
+
result["tierDowngradeCount"] = len(tier_downgrades)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- CLI --------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _build_parser():
|
|
198
|
+
parser = argparse.ArgumentParser(
|
|
199
|
+
prog="skf-report-delta",
|
|
200
|
+
description=(
|
|
201
|
+
"Deterministic previous-vs-current feasibility delta (synthesize.md "
|
|
202
|
+
"§3). Consumes the two extracted finding sets and emits improved / "
|
|
203
|
+
"regressed / unchanged / new / dropped counts plus tier downgrades."
|
|
204
|
+
),
|
|
205
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
206
|
+
epilog=(
|
|
207
|
+
"Example:\n"
|
|
208
|
+
" uv run skf-report-delta.py "
|
|
209
|
+
"'{\"previous\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Missing\"}]},"
|
|
210
|
+
"\"current\":{\"coverage\":[{\"technology\":\"react\",\"verdict\":\"Covered\"}]}}'"
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
src = parser.add_mutually_exclusive_group()
|
|
214
|
+
src.add_argument("json_input", nargs="?", help="JSON object as a positional argument.")
|
|
215
|
+
src.add_argument("--json-input", dest="json_input_flag", help="JSON object passed via flag.")
|
|
216
|
+
src.add_argument("--stdin", action="store_true", help="Read the JSON object from stdin.")
|
|
217
|
+
return parser
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _resolve_input(args):
|
|
221
|
+
if args.stdin:
|
|
222
|
+
return sys.stdin.read()
|
|
223
|
+
if args.json_input_flag is not None:
|
|
224
|
+
return args.json_input_flag
|
|
225
|
+
if args.json_input is not None:
|
|
226
|
+
return args.json_input
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def main(argv=None):
|
|
231
|
+
parser = _build_parser()
|
|
232
|
+
args = parser.parse_args(argv)
|
|
233
|
+
raw = _resolve_input(args)
|
|
234
|
+
if not raw.strip():
|
|
235
|
+
parser.print_usage(file=sys.stderr)
|
|
236
|
+
print(
|
|
237
|
+
"error: no input provided (positional arg, --json-input, or --stdin)",
|
|
238
|
+
file=sys.stderr,
|
|
239
|
+
)
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
data = json.loads(raw)
|
|
244
|
+
except json.JSONDecodeError as exc:
|
|
245
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
result = compute(data)
|
|
249
|
+
print(json.dumps(result, indent=2))
|
|
250
|
+
if isinstance(result, dict) and result.get("code") == "INVALID_INPUT":
|
|
251
|
+
return 2
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
raise SystemExit(main())
|