bmad-module-skill-forge 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,22 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# dependencies = []
|
|
5
5
|
# ///
|
|
6
6
|
"""Deterministic contextual-coherence aggregator.
|
|
7
7
|
|
|
8
8
|
Pure-function tally + weighted mean for the SKF test-skill workflow
|
|
9
9
|
(step-04, coherence-check.md §5c). The prompt keeps the judgment — deciding
|
|
10
|
-
which
|
|
11
|
-
complete (§5). This script does
|
|
12
|
-
the
|
|
10
|
+
which skill, type-import and integration-pattern references are accurate
|
|
11
|
+
(§4) and which integration patterns are complete (§5). This script does the
|
|
12
|
+
counting and the arithmetic those judgments feed: it counts the valid
|
|
13
|
+
references from the per-reference results, then computes the
|
|
14
|
+
reference-validity ratio, the integration-completeness ratio, and their
|
|
13
15
|
fixed 0.6 / 0.4 weighted mean. That way the 18%-weight `coherence` input to
|
|
14
16
|
compute-score.py is computed once, deterministically, instead of by hand on
|
|
15
17
|
every run (this skill grades other skills — a false PASS is catastrophic, so
|
|
16
18
|
every scoring input is scripted for run-to-run reproducibility).
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
Per-reference input (the files coherence-check.md §5c passes):
|
|
21
|
+
--references skf-scan-skill-md-structure.py reference-check output: its
|
|
22
|
+
`references[]` hold the file-path and script/asset
|
|
23
|
+
references, each valid when its `status` is "ok"
|
|
24
|
+
("missing": no file inside an allowed root; "escapes":
|
|
25
|
+
outside every root)
|
|
26
|
+
--judged the §4 subagent results for the other references (skill,
|
|
27
|
+
type-import, integration-pattern), as the subagent returned
|
|
28
|
+
them (a wrapping markdown fence is stripped): a JSON array,
|
|
29
|
+
or an object with a `references` array, of
|
|
30
|
+
{"reference", "line", "target_exists", "type_match",
|
|
31
|
+
"signature_match", "issues"}; valid when the three
|
|
32
|
+
booleans are true and `issues` is empty. Optional: a skill
|
|
33
|
+
with no such reference has no file.
|
|
34
|
+
--integration the §5 integration JSON ({"patterns_documented",
|
|
35
|
+
"patterns_complete", ...}, fence stripped)
|
|
36
|
+
`total_references` is the number of entries in both, `valid_references`
|
|
37
|
+
the number of valid ones, and `invalidReferences[]` lists each invalid
|
|
38
|
+
one with its `status`: "missing" or "escapes" from the scan, and for a
|
|
39
|
+
judged one "missing" (target_exists false) or "inaccurate". Each entry
|
|
40
|
+
also carries what coherence-check.md §5c needs for its gap, so the
|
|
41
|
+
caller never matches it back to the per-reference files: a scanned
|
|
42
|
+
one's `canonical` (the realpath the scanner checked, which an escape
|
|
43
|
+
names) and `root`, and a judged one's `issues` (when the subagent gave
|
|
44
|
+
none, the flags that are false, such as "type_match: false").
|
|
45
|
+
|
|
46
|
+
Formula (this script is its one home: scoring-rules.md points here, and
|
|
47
|
+
coherence-check.md §5 defines what makes a pattern complete):
|
|
20
48
|
|
|
21
49
|
reference_validity = (valid_references / total_references) * 100
|
|
22
50
|
integration_completeness = (complete_patterns / total_patterns) * 100
|
|
@@ -27,7 +55,7 @@ Field-name mapping: the step-04 §5 integration JSON calls the pattern counts
|
|
|
27
55
|
(= complete_patterns); this script's input uses those step-04 names directly
|
|
28
56
|
so §5c passes them through unrenamed.
|
|
29
57
|
|
|
30
|
-
Edge cases (
|
|
58
|
+
Edge cases (absence is never penalized):
|
|
31
59
|
* patterns_documented == 0 -> no integration patterns to weigh, so
|
|
32
60
|
combined_coherence == reference_validity and integrationCompleteness is
|
|
33
61
|
null. (Do not divide by zero.)
|
|
@@ -46,11 +74,18 @@ Input schema (one JSON object):
|
|
|
46
74
|
|
|
47
75
|
Output (JSON):
|
|
48
76
|
{
|
|
49
|
-
"input": { ...echo... },
|
|
77
|
+
"input": { ...echo, or the counts the files gave... },
|
|
50
78
|
"referenceValidity": <0-100>,
|
|
51
79
|
"integrationCompleteness": <0-100 | null when patterns_documented == 0>,
|
|
52
80
|
"combinedCoherence": <0-100>,
|
|
53
|
-
"patternsScored": <bool
|
|
81
|
+
"patternsScored": <bool>,
|
|
82
|
+
"invalidReferences": [{"source": "scan" | "judged", "line": N | null,
|
|
83
|
+
"target": "...", "status": "...",
|
|
84
|
+
"canonical": "..." | null, "root": "..." | null,
|
|
85
|
+
"issues": ["..."]}]
|
|
86
|
+
# with the per-reference files only; canonical
|
|
87
|
+
# and root are null for a judged entry, and
|
|
88
|
+
# issues is empty for a scanned one
|
|
54
89
|
}
|
|
55
90
|
or {"error": ..., "code": "INVALID_INPUT"} on a schema violation.
|
|
56
91
|
|
|
@@ -58,14 +93,20 @@ Percentages use the same JavaScript-compatible 2-decimal rounding as
|
|
|
58
93
|
compute-score.py so the two scoring scripts agree to the last digit.
|
|
59
94
|
|
|
60
95
|
CLI usage (mirrors compute-score.py):
|
|
61
|
-
uv run aggregate-coherence.py
|
|
62
|
-
uv run aggregate-coherence.py
|
|
63
|
-
|
|
96
|
+
uv run aggregate-coherence.py --references <scan.json> [--judged <file>] --integration <file>
|
|
97
|
+
uv run aggregate-coherence.py '<JSON>' # counts, positional arg
|
|
98
|
+
uv run aggregate-coherence.py --json-input '<JSON>' # counts, explicit flag form
|
|
99
|
+
cat input.json | uv run aggregate-coherence.py --stdin # counts, piped input
|
|
100
|
+
|
|
101
|
+
--output <file> also writes the result to a file (UTF-8 JSON), the one
|
|
102
|
+
compute-score.py --coherence reads; a refused input removes a file left
|
|
103
|
+
there earlier.
|
|
64
104
|
|
|
65
105
|
Exit codes (same convention as compute-score.py / reconcile-coverage.py):
|
|
66
|
-
0
|
|
67
|
-
1
|
|
68
|
-
|
|
106
|
+
0 a result object was emitted
|
|
107
|
+
1 input could not be parsed at all (no input provided, malformed JSON,
|
|
108
|
+
or a file that cannot be read)
|
|
109
|
+
2 input parsed but schema/semantics invalid
|
|
69
110
|
|
|
70
111
|
Both 1 and 2 emit an {"error": ..., "code": "INVALID_INPUT"} envelope on stdout,
|
|
71
112
|
so the envelope's presence — not the specific code — tells a caller the input was
|
|
@@ -75,11 +116,13 @@ refused rather than scored.
|
|
|
75
116
|
from __future__ import annotations
|
|
76
117
|
|
|
77
118
|
import argparse
|
|
119
|
+
import importlib.util
|
|
78
120
|
import json
|
|
79
121
|
import math
|
|
80
122
|
import sys
|
|
123
|
+
from pathlib import Path
|
|
81
124
|
|
|
82
|
-
# Weights for the combined-coherence mean
|
|
125
|
+
# Weights for the combined-coherence mean. Kept as
|
|
83
126
|
# named constants so the 0.6 / 0.4 split lives in exactly one place.
|
|
84
127
|
REFERENCE_VALIDITY_WEIGHT = 0.6
|
|
85
128
|
INTEGRATION_COMPLETENESS_WEIGHT = 0.4
|
|
@@ -91,6 +134,26 @@ COUNT_FIELDS = (
|
|
|
91
134
|
"patterns_complete",
|
|
92
135
|
)
|
|
93
136
|
|
|
137
|
+
# reference-check statuses (skf-scan-skill-md-structure.py); only "ok" is valid.
|
|
138
|
+
SCAN_STATUSES = ("ok", "missing", "escapes")
|
|
139
|
+
JUDGED_FLAGS = ("target_exists", "type_match", "signature_match")
|
|
140
|
+
|
|
141
|
+
_INVENTORY_MODULE = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _inventory_module():
|
|
145
|
+
"""validate-inventory.py beside this script: the fence rule lives there."""
|
|
146
|
+
global _INVENTORY_MODULE
|
|
147
|
+
if _INVENTORY_MODULE is None:
|
|
148
|
+
sibling = Path(__file__).resolve().parent / "validate-inventory.py"
|
|
149
|
+
spec = importlib.util.spec_from_file_location("skf_validate_inventory", sibling)
|
|
150
|
+
if spec is None or spec.loader is None or not sibling.is_file():
|
|
151
|
+
raise ImportError(f"validate-inventory.py not found beside {Path(__file__).name}")
|
|
152
|
+
module = importlib.util.module_from_spec(spec)
|
|
153
|
+
spec.loader.exec_module(module)
|
|
154
|
+
_INVENTORY_MODULE = module
|
|
155
|
+
return _INVENTORY_MODULE
|
|
156
|
+
|
|
94
157
|
|
|
95
158
|
def round2(value):
|
|
96
159
|
"""Round to 2 decimals with JavaScript-compatible half-up rounding.
|
|
@@ -207,6 +270,98 @@ def aggregate_coherence(inp):
|
|
|
207
270
|
}
|
|
208
271
|
|
|
209
272
|
|
|
273
|
+
def count_references(scan, judged):
|
|
274
|
+
"""Count the valid references from the per-reference results.
|
|
275
|
+
|
|
276
|
+
`scan` is the reference-check output (an object with `references[]`),
|
|
277
|
+
`judged` the list of subagent results (or None). Returns
|
|
278
|
+
(valid, total, invalid[]) or an error string.
|
|
279
|
+
|
|
280
|
+
>>> scan = {"references": [{"line": 3, "target": "a.md", "status": "ok"},
|
|
281
|
+
... {"line": 5, "target": "../x", "status": "escapes"}]}
|
|
282
|
+
>>> judged = [{"reference": "react", "line": 9, "target_exists": True, "type_match": True,
|
|
283
|
+
... "signature_match": True, "issues": []},
|
|
284
|
+
... {"reference": "./types", "line": 12, "target_exists": True, "type_match": False,
|
|
285
|
+
... "signature_match": True, "issues": []}]
|
|
286
|
+
>>> valid, total, invalid = count_references(scan, judged)
|
|
287
|
+
>>> valid, total, [(i["target"], i["status"]) for i in invalid]
|
|
288
|
+
(2, 4, [('../x', 'escapes'), ('./types', 'inaccurate')])
|
|
289
|
+
>>> invalid[1]["issues"], invalid[1]["canonical"]
|
|
290
|
+
(['type_match: false'], None)
|
|
291
|
+
"""
|
|
292
|
+
refs = scan.get("references") if isinstance(scan, dict) else None
|
|
293
|
+
if not isinstance(refs, list):
|
|
294
|
+
return "--references holds no `references` array (pass the reference-check output)"
|
|
295
|
+
valid, invalid = 0, []
|
|
296
|
+
for index, ref in enumerate(refs):
|
|
297
|
+
if not isinstance(ref, dict) or ref.get("status") not in SCAN_STATUSES:
|
|
298
|
+
return f"--references references[{index}] has no status in {list(SCAN_STATUSES)}"
|
|
299
|
+
if ref["status"] == "ok":
|
|
300
|
+
valid += 1
|
|
301
|
+
else:
|
|
302
|
+
invalid.append({"source": "scan", "line": ref.get("line"),
|
|
303
|
+
"target": ref.get("target"), "status": ref["status"],
|
|
304
|
+
"canonical": ref.get("canonical"), "root": ref.get("root"),
|
|
305
|
+
"issues": []})
|
|
306
|
+
for index, ref in enumerate(judged or []):
|
|
307
|
+
if not isinstance(ref, dict):
|
|
308
|
+
return f"--judged [{index}] is not an object"
|
|
309
|
+
for flag in JUDGED_FLAGS:
|
|
310
|
+
if not isinstance(ref.get(flag), bool):
|
|
311
|
+
return f"--judged [{index}] `{flag}` must be true or false"
|
|
312
|
+
issues = ref.get("issues", [])
|
|
313
|
+
if not isinstance(issues, list):
|
|
314
|
+
return f"--judged [{index}] `issues` must be a list"
|
|
315
|
+
if all(ref[flag] for flag in JUDGED_FLAGS) and not issues:
|
|
316
|
+
valid += 1
|
|
317
|
+
else:
|
|
318
|
+
invalid.append({"source": "judged", "line": ref.get("line"), "target": ref.get("reference"),
|
|
319
|
+
"status": "missing" if not ref["target_exists"] else "inaccurate",
|
|
320
|
+
"canonical": None, "root": None,
|
|
321
|
+
"issues": [str(i) for i in issues]
|
|
322
|
+
or [f"{flag}: false" for flag in JUDGED_FLAGS if not ref[flag]]})
|
|
323
|
+
return valid, len(refs) + len(judged or []), invalid
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _read_json_file(path, label, fenced=False):
|
|
327
|
+
"""Parse a JSON file; a subagent response may come wrapped in a markdown fence."""
|
|
328
|
+
text = Path(path).read_bytes().decode("utf-8-sig")
|
|
329
|
+
if fenced:
|
|
330
|
+
text = _inventory_module().strip_fences(text)
|
|
331
|
+
try:
|
|
332
|
+
return json.loads(text)
|
|
333
|
+
except json.JSONDecodeError as exc:
|
|
334
|
+
raise ValueError(f"{label} {path} is not valid JSON: {exc.msg}") from exc
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def aggregate_from_files(references, judged=None, integration=None):
|
|
338
|
+
"""Aggregate from the per-reference files and the §5 integration JSON."""
|
|
339
|
+
scan = _read_json_file(references, "--references")
|
|
340
|
+
results = None
|
|
341
|
+
if judged is not None:
|
|
342
|
+
results = _read_json_file(judged, "--judged", fenced=True)
|
|
343
|
+
if isinstance(results, dict):
|
|
344
|
+
results = results.get("references")
|
|
345
|
+
if not isinstance(results, list):
|
|
346
|
+
return make_error(f"--judged {judged} holds no array of reference results")
|
|
347
|
+
patterns = _read_json_file(integration, "--integration", fenced=True)
|
|
348
|
+
if not isinstance(patterns, dict):
|
|
349
|
+
return make_error(f"--integration {integration} is not a JSON object")
|
|
350
|
+
counted = count_references(scan, results)
|
|
351
|
+
if isinstance(counted, str):
|
|
352
|
+
return make_error(counted)
|
|
353
|
+
valid, total, invalid = counted
|
|
354
|
+
result = aggregate_coherence({
|
|
355
|
+
"valid_references": valid,
|
|
356
|
+
"total_references": total,
|
|
357
|
+
"patterns_documented": patterns.get("patterns_documented"),
|
|
358
|
+
"patterns_complete": patterns.get("patterns_complete"),
|
|
359
|
+
})
|
|
360
|
+
if result.get("code") != "INVALID_INPUT":
|
|
361
|
+
result["invalidReferences"] = invalid
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
|
|
210
365
|
def _build_parser() -> argparse.ArgumentParser:
|
|
211
366
|
parser = argparse.ArgumentParser(
|
|
212
367
|
prog="aggregate-coherence",
|
|
@@ -240,9 +395,25 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
240
395
|
action="store_true",
|
|
241
396
|
help="Read the JSON object from stdin.",
|
|
242
397
|
)
|
|
398
|
+
parser.add_argument("--references", metavar="PATH",
|
|
399
|
+
help="skf-scan-skill-md-structure.py reference-check output")
|
|
400
|
+
parser.add_argument("--judged", metavar="PATH",
|
|
401
|
+
help="the subagent results for the skill, type-import and integration references")
|
|
402
|
+
parser.add_argument("--integration", metavar="PATH",
|
|
403
|
+
help="the integration JSON (patterns_documented, patterns_complete)")
|
|
404
|
+
parser.add_argument("--output", metavar="PATH", help="also write the result to this file")
|
|
243
405
|
return parser
|
|
244
406
|
|
|
245
407
|
|
|
408
|
+
def _write_output(path, result):
|
|
409
|
+
target = Path(path)
|
|
410
|
+
if result.get("code") == "INVALID_INPUT":
|
|
411
|
+
target.unlink(missing_ok=True)
|
|
412
|
+
return
|
|
413
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
target.write_bytes((json.dumps(result, indent=2) + "\n").encode("utf-8"))
|
|
415
|
+
|
|
416
|
+
|
|
246
417
|
def _resolve_input(args: argparse.Namespace) -> str:
|
|
247
418
|
if args.stdin:
|
|
248
419
|
return sys.stdin.read()
|
|
@@ -256,22 +427,39 @@ def _resolve_input(args: argparse.Namespace) -> str:
|
|
|
256
427
|
def main(argv: list[str] | None = None) -> int:
|
|
257
428
|
parser = _build_parser()
|
|
258
429
|
args = parser.parse_args(argv)
|
|
259
|
-
|
|
260
|
-
if not
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
430
|
+
files = (args.references, args.judged, args.integration)
|
|
431
|
+
if any(f is not None for f in files):
|
|
432
|
+
if args.stdin or args.json_input_flag is not None or args.json_input is not None:
|
|
433
|
+
parser.error("pass the counts or the files (--references, --judged, --integration), not both")
|
|
434
|
+
if args.references is None or args.integration is None:
|
|
435
|
+
parser.error("--references and --integration go together (--judged is optional)")
|
|
436
|
+
try:
|
|
437
|
+
result = aggregate_from_files(*files)
|
|
438
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
439
|
+
print(json.dumps(make_error(str(exc)), indent=2))
|
|
440
|
+
if args.output:
|
|
441
|
+
Path(args.output).unlink(missing_ok=True)
|
|
442
|
+
return 1
|
|
443
|
+
else:
|
|
444
|
+
raw = _resolve_input(args)
|
|
445
|
+
if not raw.strip():
|
|
446
|
+
parser.print_usage(file=sys.stderr)
|
|
447
|
+
print(
|
|
448
|
+
"error: no input provided (--references and --integration, positional arg, "
|
|
449
|
+
"--json-input, or --stdin)",
|
|
450
|
+
file=sys.stderr,
|
|
451
|
+
)
|
|
452
|
+
return 1
|
|
267
453
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
454
|
+
try:
|
|
455
|
+
data = json.loads(raw)
|
|
456
|
+
except json.JSONDecodeError as exc:
|
|
457
|
+
print(json.dumps(make_error(f"Invalid JSON: {exc.msg}"), indent=2))
|
|
458
|
+
return 1
|
|
273
459
|
|
|
274
|
-
|
|
460
|
+
result = aggregate_coherence(data)
|
|
461
|
+
if args.output:
|
|
462
|
+
_write_output(args.output, result)
|
|
275
463
|
print(json.dumps(result, indent=2))
|
|
276
464
|
# Same convention as compute-score.py / reconcile-coverage.py. This script
|
|
277
465
|
# produces the coherence percentage that score.md §3a feeds to
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF test-skill result context: the emitter payload of a finished run.
|
|
7
|
+
|
|
8
|
+
report.md §4c hands the shared emitter (`skf-emit-result-envelope.py emit
|
|
9
|
+
--workflow skf-test-skill`) one payload, which the emitter turns into the
|
|
10
|
+
SKF_TEST_RESULT_JSON line and the run's result files. Every value in it
|
|
11
|
+
follows from records the run already wrote, so this script builds it rather
|
|
12
|
+
than the model retyping them from memory:
|
|
13
|
+
|
|
14
|
+
- the report frontmatter: skillName, runId, testMode, hardGate,
|
|
15
|
+
testResult, score, threshold, thresholdFallback, originalThreshold and
|
|
16
|
+
evidenceReportPath (the steps write the verdict inputs there, so a
|
|
17
|
+
compacted context cannot change them);
|
|
18
|
+
- the gap ledger: the gap counts by severity (gap-ledger.py's summary);
|
|
19
|
+
- the scoring script's output, which score.md saves: activeCategories and
|
|
20
|
+
inconclusiveReasons. A run the hard gate blocked was never scored and
|
|
21
|
+
has none.
|
|
22
|
+
|
|
23
|
+
The derived fields live here, in one place:
|
|
24
|
+
|
|
25
|
+
testResult verdict summary.result exit_code next_workflow
|
|
26
|
+
pass PASS PASS 0 export-skill
|
|
27
|
+
pass-with-drift pass-with-drift PASS_WITH_DRIFT 4 update-skill
|
|
28
|
+
fail FAIL FAIL 2 update-skill
|
|
29
|
+
inconclusive INCONCLUSIVE INCONCLUSIVE 3 null
|
|
30
|
+
|
|
31
|
+
A run the hard gate blocked (frontmatter hardGate: 'blocked') takes the
|
|
32
|
+
hard gate's own payload (hard-gate.py `blocked_envelope`): status error,
|
|
33
|
+
halt_reason hard-gate-blocked, verdict FAIL, exit 2, score and threshold
|
|
34
|
+
null. threshold_fallback and original_threshold appear only when the
|
|
35
|
+
threshold fallback fired: absent, never false or null, otherwise.
|
|
36
|
+
|
|
37
|
+
Payload (written to --output, UTF-8 JSON):
|
|
38
|
+
{<the envelope fields: status, skill_name, verdict, score, threshold,
|
|
39
|
+
report_path, next_workflow, exit_code, halt_reason, and
|
|
40
|
+
threshold_fallback and original_threshold when the fallback fired>,
|
|
41
|
+
"warnings": [<each --warning, in order>] (only when one is given),
|
|
42
|
+
"result_contract": {"skill": "skf-test-skill", <the envelope fields>,
|
|
43
|
+
"outputs": [{"type": "report", "path": <report>}],
|
|
44
|
+
"summary": {"score", "threshold", "result", "testMode",
|
|
45
|
+
"activeCategories", "inconclusiveReasons" (INCONCLUSIVE),
|
|
46
|
+
"threshold_fallback", "original_threshold",
|
|
47
|
+
"evidence_report_path" (fallback only), "gapCounts",
|
|
48
|
+
"hardGate"},
|
|
49
|
+
"runId": <runId>, "healthCheckDispatched": <bool>}}
|
|
50
|
+
|
|
51
|
+
CLI:
|
|
52
|
+
uv run build-result-context.py --report <report.md> --ledger <ledger.json>
|
|
53
|
+
[--score <scoring output>] [--no-health-check] [--warning <text>]...
|
|
54
|
+
--output <result-context.json>
|
|
55
|
+
|
|
56
|
+
--warning hands the emitter a warning the run kept for its result (such as
|
|
57
|
+
`customization_resolver_unavailable: <reason>`); the emitter adds the ones
|
|
58
|
+
the run recorded in its sink, and stamps all of them into the envelope and
|
|
59
|
+
the result files.
|
|
60
|
+
|
|
61
|
+
stdout (one object): {"status", "verdict", "exit_code", "next_workflow",
|
|
62
|
+
"halt_reason", "target", "output"}; `target` is the --target the emit call
|
|
63
|
+
takes: "stderr" for a blocked run, else "stdout".
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 the payload was written
|
|
67
|
+
1 an input is missing or unreadable, the report's frontmatter holds no
|
|
68
|
+
settled verdict, or --output cannot be written ({"status": "error",
|
|
69
|
+
"error"} on stdout)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
import argparse
|
|
75
|
+
import importlib.util
|
|
76
|
+
import json
|
|
77
|
+
import re
|
|
78
|
+
import sys
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
WORKFLOW = "skf-test-skill"
|
|
82
|
+
# testResult -> (verdict, summary.result, exit_code, next_workflow)
|
|
83
|
+
VERDICTS = {
|
|
84
|
+
"pass": ("PASS", "PASS", 0, "export-skill"),
|
|
85
|
+
"pass-with-drift": ("pass-with-drift", "PASS_WITH_DRIFT", 4, "update-skill"),
|
|
86
|
+
"fail": ("FAIL", "FAIL", 2, "update-skill"),
|
|
87
|
+
"inconclusive": ("INCONCLUSIVE", "INCONCLUSIVE", 3, None),
|
|
88
|
+
}
|
|
89
|
+
_KEY_RE = re.compile(r"^([A-Za-z_][\w-]*):\s*(.*?)\s*$")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ContextError(Exception):
|
|
93
|
+
"""An input the payload cannot be built from: exit 1."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sibling(name: str, module: str):
|
|
97
|
+
"""Import a script that sits beside this one (gap-ledger.py, hard-gate.py)."""
|
|
98
|
+
path = Path(__file__).resolve().parent / name
|
|
99
|
+
spec = importlib.util.spec_from_file_location(module, path)
|
|
100
|
+
if spec is None or spec.loader is None or not path.is_file():
|
|
101
|
+
raise ContextError(f"{name} not found beside {Path(__file__).name}")
|
|
102
|
+
loaded = importlib.util.module_from_spec(spec)
|
|
103
|
+
spec.loader.exec_module(loaded)
|
|
104
|
+
return loaded
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_frontmatter(text: str) -> dict[str, str]:
|
|
108
|
+
"""Top-level scalar keys of the leading `---` block, quotes removed.
|
|
109
|
+
|
|
110
|
+
>>> read_frontmatter("---\\ntestResult: 'pass'\\nscore: 91.5%\\nsteps: ['a']\\n---\\n# R")
|
|
111
|
+
{'testResult': 'pass', 'score': '91.5%', 'steps': "['a']"}
|
|
112
|
+
>>> read_frontmatter("# no frontmatter")
|
|
113
|
+
{}
|
|
114
|
+
"""
|
|
115
|
+
lines = text.lstrip("").split("\n")
|
|
116
|
+
if not lines or lines[0].strip() != "---":
|
|
117
|
+
return {}
|
|
118
|
+
out: dict[str, str] = {}
|
|
119
|
+
for line in lines[1:]:
|
|
120
|
+
if line.strip() == "---":
|
|
121
|
+
return out
|
|
122
|
+
m = _KEY_RE.match(line.rstrip("\r"))
|
|
123
|
+
if not m:
|
|
124
|
+
continue
|
|
125
|
+
value = m.group(2)
|
|
126
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
|
127
|
+
value = value[1:-1]
|
|
128
|
+
out[m.group(1)] = value
|
|
129
|
+
return {}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def percent(value: str | None):
|
|
133
|
+
"""'87.5%' -> 87.5, '90%' or '90' -> 90, '' or None -> None.
|
|
134
|
+
|
|
135
|
+
>>> percent("87.5%"), percent("90"), percent(""), percent(None)
|
|
136
|
+
(87.5, 90, None, None)
|
|
137
|
+
"""
|
|
138
|
+
text = (value or "").strip().rstrip("%").strip()
|
|
139
|
+
if not text:
|
|
140
|
+
return None
|
|
141
|
+
try:
|
|
142
|
+
number = float(text)
|
|
143
|
+
except ValueError:
|
|
144
|
+
raise ContextError(f"not a percentage: {value!r}") from None
|
|
145
|
+
return int(number) if number.is_integer() else number
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read_json(path: str, label: str):
|
|
149
|
+
try:
|
|
150
|
+
return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
151
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
152
|
+
raise ContextError(f"{label} {path} cannot be read: {exc}") from exc
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build(report: str, ledger: str, score: str | None = None, health_check: bool = True,
|
|
156
|
+
warnings: list[str] | None = None) -> dict:
|
|
157
|
+
"""The emitter payload for the run whose report and ledger are given."""
|
|
158
|
+
try:
|
|
159
|
+
front = read_frontmatter(Path(report).read_bytes().decode("utf-8-sig"))
|
|
160
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
161
|
+
raise ContextError(f"--report {report} cannot be read: {exc}") from exc
|
|
162
|
+
if not front:
|
|
163
|
+
raise ContextError(f"--report {report} has no frontmatter")
|
|
164
|
+
gl = _sibling("gap-ledger.py", "skf_gap_ledger")
|
|
165
|
+
try:
|
|
166
|
+
counts = gl.summarize(Path(ledger), gl.load_ledger(Path(ledger)))["counts"]
|
|
167
|
+
except gl.LedgerError as exc:
|
|
168
|
+
raise ContextError(f"--ledger {ledger}: {exc}") from exc
|
|
169
|
+
|
|
170
|
+
skill = front.get("skillName") or None
|
|
171
|
+
blocked = front.get("hardGate") == "blocked"
|
|
172
|
+
if blocked:
|
|
173
|
+
envelope = dict(_sibling("hard-gate.py", "skf_hard_gate").blocked_envelope(skill, report))
|
|
174
|
+
result, active, inconclusive = "FAIL", [], None
|
|
175
|
+
else:
|
|
176
|
+
test_result = front.get("testResult", "")
|
|
177
|
+
if test_result not in VERDICTS:
|
|
178
|
+
raise ContextError(f"--report {report} holds no settled verdict (testResult {test_result!r})")
|
|
179
|
+
verdict, result, exit_code, next_workflow = VERDICTS[test_result]
|
|
180
|
+
envelope = {
|
|
181
|
+
"status": "success",
|
|
182
|
+
"skill_name": skill,
|
|
183
|
+
"verdict": verdict,
|
|
184
|
+
"score": percent(front.get("score")),
|
|
185
|
+
"threshold": percent(front.get("threshold")),
|
|
186
|
+
"report_path": report,
|
|
187
|
+
"next_workflow": next_workflow,
|
|
188
|
+
"exit_code": exit_code,
|
|
189
|
+
"halt_reason": None,
|
|
190
|
+
}
|
|
191
|
+
scored = _read_json(score, "--score") if score else {}
|
|
192
|
+
if not isinstance(scored, dict):
|
|
193
|
+
raise ContextError(f"--score {score} is not a JSON object")
|
|
194
|
+
active = scored.get("activeCategories", [])
|
|
195
|
+
inconclusive = scored.get("inconclusiveReasons") if verdict == "INCONCLUSIVE" else None
|
|
196
|
+
|
|
197
|
+
fallback = front.get("thresholdFallback", "").lower() == "true" and not blocked
|
|
198
|
+
if fallback:
|
|
199
|
+
envelope["threshold_fallback"] = True
|
|
200
|
+
envelope["original_threshold"] = percent(front.get("originalThreshold"))
|
|
201
|
+
|
|
202
|
+
summary = {
|
|
203
|
+
"score": envelope["score"],
|
|
204
|
+
"threshold": envelope["threshold"],
|
|
205
|
+
"result": result,
|
|
206
|
+
"testMode": front.get("testMode") or None,
|
|
207
|
+
"activeCategories": active,
|
|
208
|
+
}
|
|
209
|
+
if inconclusive is not None:
|
|
210
|
+
summary["inconclusiveReasons"] = inconclusive
|
|
211
|
+
if fallback:
|
|
212
|
+
summary["threshold_fallback"] = True
|
|
213
|
+
summary["original_threshold"] = envelope["original_threshold"]
|
|
214
|
+
summary["evidence_report_path"] = front.get("evidenceReportPath") or None
|
|
215
|
+
summary["gapCounts"] = counts
|
|
216
|
+
summary["hardGate"] = front.get("hardGate") or None
|
|
217
|
+
|
|
218
|
+
contract = {"skill": WORKFLOW, **envelope,
|
|
219
|
+
"outputs": [{"type": "report", "path": report}],
|
|
220
|
+
"summary": summary,
|
|
221
|
+
"runId": front.get("runId") or None,
|
|
222
|
+
"healthCheckDispatched": health_check}
|
|
223
|
+
payload = {**envelope, "result_contract": contract}
|
|
224
|
+
kept = [w.strip() for w in warnings or [] if w and w.strip()]
|
|
225
|
+
if kept:
|
|
226
|
+
payload["warnings"] = kept
|
|
227
|
+
return payload
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
231
|
+
parser = argparse.ArgumentParser(
|
|
232
|
+
prog="build-result-context",
|
|
233
|
+
description=("Build the shared emitter's payload for a finished test-skill run from its "
|
|
234
|
+
"report frontmatter, its gap ledger and the scoring output."),
|
|
235
|
+
)
|
|
236
|
+
parser.add_argument("--report", required=True, help="the test report, at its published name")
|
|
237
|
+
parser.add_argument("--ledger", required=True, help="the run's gap ledger")
|
|
238
|
+
parser.add_argument("--score", help="the scoring script's output (absent for a blocked run)")
|
|
239
|
+
parser.add_argument("--no-health-check", action="store_true",
|
|
240
|
+
help="the run was given --no-health-check (healthCheckDispatched false)")
|
|
241
|
+
parser.add_argument("--warning", action="append", default=[],
|
|
242
|
+
help="a warning the run kept for its result (repeatable)")
|
|
243
|
+
parser.add_argument("--output", required=True, help="where to write the payload")
|
|
244
|
+
return parser
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main(argv: list[str] | None = None) -> int:
|
|
248
|
+
args = _build_parser().parse_args(argv)
|
|
249
|
+
try:
|
|
250
|
+
payload = build(args.report, args.ledger, args.score, not args.no_health_check, args.warning)
|
|
251
|
+
out = Path(args.output)
|
|
252
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
out.write_bytes((json.dumps(payload, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
254
|
+
except (ContextError, OSError) as exc:
|
|
255
|
+
print(json.dumps({"status": "error", "error": str(exc)}))
|
|
256
|
+
return 1
|
|
257
|
+
blocked = payload["halt_reason"] is not None
|
|
258
|
+
print(json.dumps({
|
|
259
|
+
"status": payload["status"],
|
|
260
|
+
"verdict": payload["verdict"],
|
|
261
|
+
"exit_code": payload["exit_code"],
|
|
262
|
+
"next_workflow": payload["next_workflow"],
|
|
263
|
+
"halt_reason": payload["halt_reason"],
|
|
264
|
+
"target": "stderr" if blocked else "stdout",
|
|
265
|
+
"output": out.as_posix(),
|
|
266
|
+
}, indent=2))
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _force_utf8(*streams) -> None:
|
|
271
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
272
|
+
|
|
273
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
274
|
+
of the --help text, so --help would stop with UnicodeEncodeError.
|
|
275
|
+
"""
|
|
276
|
+
for stream in streams:
|
|
277
|
+
if hasattr(stream, "reconfigure"):
|
|
278
|
+
errors = getattr(stream, "errors", None)
|
|
279
|
+
if errors is None:
|
|
280
|
+
stream.reconfigure(encoding="utf-8")
|
|
281
|
+
else:
|
|
282
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
287
|
+
sys.exit(main())
|