bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
- package/src/shared/scripts/skf-detect-docs.py +892 -4
- 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 +215 -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 +17 -6
- 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 +55 -18
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
- 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 -89
- package/src/skf-setup/references/detect-and-tier.md +58 -66
- 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 -645
- 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,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# ///
|
|
5
|
-
"""SKF Validate Feasibility Report
|
|
5
|
+
"""SKF Validate Feasibility Report: deterministic structure/schema gate and locator.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Checks and reads a skf-verify-stack feasibility report (.md) against the
|
|
8
|
+
shared feasibility-report schema, so neither the producer nor a consumer
|
|
9
|
+
re-reads the file in prose. Given a report path (validate mode) it checks
|
|
10
|
+
that report; given --locate it first finds the project's report. Both modes
|
|
11
|
+
check and read a report the same way (read_report()), so an explicit path
|
|
12
|
+
and a located report give the same keys:
|
|
10
13
|
|
|
11
14
|
1. The five required body sections are all present and appear in the
|
|
12
15
|
canonical order defined by the shared feasibility-report schema:
|
|
@@ -16,37 +19,106 @@ eyeballed by re-reading the file each run:
|
|
|
16
19
|
## Recommendations
|
|
17
20
|
## Evidence Sources
|
|
18
21
|
2. Frontmatter `schemaVersion` equals the literal producer version "1.0".
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
3. A report whose schemaVersion is not "1.0" is not interpreted: nothing
|
|
23
|
+
below is read from it. Otherwise overallVerdict, generatedAt and
|
|
24
|
+
coveragePercentage come from the frontmatter, and pairVerdicts from the
|
|
25
|
+
canonical `| lib_a | lib_b | verdict | rationale |` table under
|
|
26
|
+
`## Integration Verdicts`, and only that table (the display table with
|
|
27
|
+
more columns below it is not read). Fenced code and HTML comments are
|
|
28
|
+
skipped, so an example table in either is not read. The report holds
|
|
29
|
+
the canonical table once: a second one is a schema violation
|
|
30
|
+
(duplicateVerdictTableLine), since reading only the first, say the
|
|
31
|
+
template's empty table left above the filled one, would drop every
|
|
32
|
+
pair. Verdict tokens are case-sensitive: a token outside the schema's
|
|
33
|
+
set is listed in unknownTokens, never dropped or mapped, and its row
|
|
34
|
+
stays in pairVerdicts as written.
|
|
35
|
+
4. coveragePercentage is the frontmatter value when it is a whole number
|
|
36
|
+
from 0 to 100, else null. coverageMeasured is true only when the
|
|
37
|
+
frontmatter's stepsCompleted list names `coverage` and
|
|
38
|
+
coveragePercentage is not null: the producer writes
|
|
39
|
+
`coveragePercentage: 0` when a run starts, so a run that stopped before
|
|
40
|
+
its coverage step holds a 0 that was never measured.
|
|
41
|
+
|
|
42
|
+
Every check has one right answer for a given file, so it belongs in a
|
|
43
|
+
script, not in an LLM re-read. Frontmatter is parsed with the standard
|
|
44
|
+
library only (no pyyaml dep) using the same delimiter handling as
|
|
45
|
+
skf-validate-output.py: a `---`-delimited block at the top of the file,
|
|
46
|
+
parsed as simple `key: value` scalars with surrounding quotes stripped, and
|
|
47
|
+
stepsCompleted read as a flow (`[a, b]`) or block (`- a`) list.
|
|
48
|
+
|
|
49
|
+
Locate mode (--locate <folder> --project-name <name>) finds the report for
|
|
50
|
+
a consumer, so no workflow builds the file name in prose:
|
|
51
|
+
|
|
52
|
+
1. The project name becomes the producer's `{project_slug}` (slugify()):
|
|
53
|
+
NFKD-decomposed with combining marks dropped, so `Café` gives `cafe`;
|
|
54
|
+
letters and digits with no ASCII form dropped; lowercased; every run of
|
|
55
|
+
other characters (spaces, punctuation, symbols) turned into one hyphen;
|
|
56
|
+
hyphens trimmed from both ends; `project` when nothing is left, which
|
|
57
|
+
includes an empty name.
|
|
58
|
+
2. Only <folder>/feasibility-report-<slug>-latest.md is read, the stable
|
|
59
|
+
copy: a timestamped report can be a halted run's partial report.
|
|
60
|
+
|
|
61
|
+
The JSON on stdout (validate mode leaves out the three locate keys, and
|
|
62
|
+
only validate mode carries schemaVersionFound, which earlier callers read):
|
|
23
63
|
|
|
24
64
|
{
|
|
25
|
-
"status": "ok" | "error",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
65
|
+
"status": "ok" | "not-found" | "error", # not-found: locate mode only
|
|
66
|
+
"projectName": "<name>", # locate mode only
|
|
67
|
+
"projectSlug": "<slug>", # locate mode only
|
|
68
|
+
"latestPath": "<folder>/feasibility-report-<slug>-latest.md", # locate mode only
|
|
69
|
+
"path": "<report path>" | null, # null when locate found no report
|
|
70
|
+
"schemaVersion": "<value>" | null,
|
|
71
|
+
"schemaVersionFound": "<value>" | null, # validate mode only
|
|
72
|
+
"schemaVersionOk": bool | null, # null when the report was not read
|
|
73
|
+
"headingsOk": bool | null,
|
|
30
74
|
"missingHeadings": [...],
|
|
31
75
|
"orderViolations": [...],
|
|
32
|
-
"
|
|
76
|
+
"generatedAt": "<value>" | null, # null when the report was not interpreted
|
|
77
|
+
"overallVerdict": "<token>" | null,
|
|
78
|
+
"coveragePercentage": int | null,
|
|
79
|
+
"coverageMeasured": bool | null,
|
|
80
|
+
"verdictTableFound": bool | null,
|
|
81
|
+
"duplicateVerdictTableLine": int | null,
|
|
82
|
+
"pairVerdicts": [{"lib_a", "lib_b", "verdict", "rationale"}, ...],
|
|
83
|
+
"unknownTokens": [{"field": "overallVerdict", "token"},
|
|
84
|
+
{"field": "verdict", "line", "lib_a", "lib_b", "token"}],
|
|
85
|
+
"violation": "schema-violation" | "io-error" | null,
|
|
86
|
+
"error": "<message>" # on io-error only
|
|
33
87
|
}
|
|
34
88
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
89
|
+
`line` is the 1-based line of the table row in the report file, and
|
|
90
|
+
duplicateVerdictTableLine the 1-based line of the second canonical table's
|
|
91
|
+
header row (null when there is none). A report validate mode cannot read
|
|
92
|
+
keeps the values earlier callers read (schemaVersionOk and headingsOk
|
|
93
|
+
false). The producer can take `projectSlug` from any locate result,
|
|
94
|
+
whatever the status.
|
|
95
|
+
|
|
96
|
+
Exit codes, validate mode (per script-standards):
|
|
97
|
+
0 valid: every check above passes (the five sections present and in
|
|
98
|
+
order, schemaVersion "1.0", the canonical table there once and every
|
|
99
|
+
verdict token known)
|
|
100
|
+
1 schema-violation: a section is missing or out of order, schemaVersion
|
|
101
|
+
is not "1.0", the canonical table is missing or there twice, or a
|
|
102
|
+
verdict token is unknown (the JSON `violation` field and the detail
|
|
103
|
+
keys carry why)
|
|
104
|
+
2 IO/parse error: the report file could not be read (violation
|
|
105
|
+
"io-error"). Also a usage error, which prints no JSON, such as a
|
|
106
|
+
second path (an unquoted report path that holds a space)
|
|
107
|
+
|
|
108
|
+
Exit codes, locate mode:
|
|
109
|
+
0 status "ok": the report exists and passes every check above. Also
|
|
110
|
+
status "not-found": there is no -latest report for this project, and
|
|
111
|
+
the caller goes on without one
|
|
112
|
+
1 schema-violation, as in validate mode
|
|
113
|
+
2 the report exists but could not be read (violation "io-error"). Also a
|
|
114
|
+
usage error, which prints no JSON: --locate without --project-name,
|
|
115
|
+
--project-name without --locate, an empty folder, or a stray argument
|
|
116
|
+
(an unquoted folder path that holds a space)
|
|
46
117
|
|
|
47
118
|
CLI:
|
|
48
119
|
python3 skf-validate-feasibility-report.py <report.md>
|
|
49
120
|
python3 skf-validate-feasibility-report.py <report.md> -o result.json
|
|
121
|
+
python3 skf-validate-feasibility-report.py --locate <folder> --project-name <name>
|
|
50
122
|
"""
|
|
51
123
|
|
|
52
124
|
from __future__ import annotations
|
|
@@ -55,6 +127,7 @@ import argparse
|
|
|
55
127
|
import json
|
|
56
128
|
import re
|
|
57
129
|
import sys
|
|
130
|
+
import unicodedata
|
|
58
131
|
from pathlib import Path
|
|
59
132
|
|
|
60
133
|
# Canonical body-section order per the shared feasibility-report schema
|
|
@@ -71,29 +144,84 @@ CANONICAL_HEADINGS = [
|
|
|
71
144
|
# Producer schema version the consumer/producer both pin to.
|
|
72
145
|
EXPECTED_SCHEMA_VERSION = "1.0"
|
|
73
146
|
|
|
147
|
+
# Verdict tokens per the shared schema; both sets are case-sensitive.
|
|
148
|
+
OVERALL_VERDICTS = ("FEASIBLE", "CONDITIONALLY_FEASIBLE", "NOT_FEASIBLE")
|
|
149
|
+
PAIR_VERDICTS = ("Verified", "Plausible", "Risky", "Blocked")
|
|
150
|
+
|
|
151
|
+
# The section that holds the pair table, and the table's fixed header row.
|
|
152
|
+
VERDICTS_HEADING = "Integration Verdicts"
|
|
153
|
+
VERDICT_TABLE_HEADER = ["lib_a", "lib_b", "verdict", "rationale"]
|
|
154
|
+
|
|
155
|
+
# The frontmatter list of the producer steps a run finished, and the step
|
|
156
|
+
# that measures coveragePercentage: a run that stopped before it holds the 0
|
|
157
|
+
# the producer writes when it starts.
|
|
158
|
+
STEPS_COMPLETED_KEY = "stepsCompleted"
|
|
159
|
+
COVERAGE_STEP = "coverage"
|
|
160
|
+
|
|
161
|
+
# The stable copy the producer writes next to each timestamped report. --locate
|
|
162
|
+
# reads only this file: a timestamped report can be a halted run's partial one.
|
|
163
|
+
LATEST_REPORT_NAME = "feasibility-report-{slug}-latest.md"
|
|
164
|
+
|
|
165
|
+
# The slug of a project name that leaves no ASCII letter or digit (a name
|
|
166
|
+
# written only in a script with no ASCII form, such as Japanese).
|
|
167
|
+
FALLBACK_SLUG = "project"
|
|
168
|
+
|
|
74
169
|
# A level-2 markdown heading: exactly two '#', then whitespace, then the text.
|
|
75
170
|
# `### Foo` does not match (the 3rd char is '#', not whitespace).
|
|
76
171
|
_H2_RE = re.compile(r"^##\s+(.+?)\s*$")
|
|
77
172
|
|
|
173
|
+
# A level 1 or 2 heading: it ends the section before it, and starts an
|
|
174
|
+
# Integration Verdicts section when its text is VERDICTS_HEADING.
|
|
175
|
+
_SECTION_END_RE = re.compile(r"^#{1,2}\s")
|
|
78
176
|
|
|
79
|
-
|
|
80
|
-
|
|
177
|
+
# The opening or closing line of a fenced code block.
|
|
178
|
+
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
|
179
|
+
|
|
180
|
+
# The first line of an HTML comment block (CommonMark HTML block type 2). The
|
|
181
|
+
# block ends on the first line that holds _COMMENT_CLOSE, which can be this one.
|
|
182
|
+
_COMMENT_OPEN_RE = re.compile(r"^\s{0,3}<!--")
|
|
183
|
+
_COMMENT_CLOSE = "-->"
|
|
184
|
+
|
|
185
|
+
# A pipe that separates table cells; `\|` is a pipe inside a cell (GFM).
|
|
186
|
+
_CELL_SEPARATOR_RE = re.compile(r"(?<!\\)\|")
|
|
187
|
+
|
|
188
|
+
# One cell of a table's delimiter row: `---`, `:--`, `--:` or `:-:`.
|
|
189
|
+
_DELIMITER_CELL_RE = re.compile(r"^:?-+:?$")
|
|
190
|
+
|
|
191
|
+
_HYPHEN_RUN_RE = re.compile(r"-+")
|
|
81
192
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
193
|
+
# coveragePercentage as the schema writes it: a whole number (0 to 100).
|
|
194
|
+
_WHOLE_NUMBER_RE = re.compile(r"[0-9]+")
|
|
195
|
+
|
|
196
|
+
# One item of a YAML block sequence: `- item`, or a lone `-` (an empty item).
|
|
197
|
+
_BLOCK_ITEM_RE = re.compile(r"^-(?:\s+(.*))?$")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _frontmatter_end(lines):
|
|
201
|
+
"""Return the index of the frontmatter's closing `---` line, or None.
|
|
202
|
+
|
|
203
|
+
The opening delimiter must be the first line, exactly `---`; the closing
|
|
204
|
+
delimiter is the next line that is exactly `---`. None when `lines` opens
|
|
205
|
+
no frontmatter block or never closes it.
|
|
87
206
|
"""
|
|
88
|
-
lines = content.split("\n")
|
|
89
207
|
if not lines or lines[0].rstrip() != "---":
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
end_idx = None
|
|
208
|
+
return None
|
|
93
209
|
for i in range(1, len(lines)):
|
|
94
210
|
if lines[i].rstrip() == "---":
|
|
95
|
-
|
|
96
|
-
|
|
211
|
+
return i
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def split_frontmatter(content):
|
|
216
|
+
"""Return (frontmatter_dict, body_text).
|
|
217
|
+
|
|
218
|
+
Stdlib-only, no YAML dependency: mirrors skf-validate-output.py's delimiter
|
|
219
|
+
handling (_frontmatter_end()). The block between the delimiters is parsed
|
|
220
|
+
as simple `key: value` scalars with surrounding quotes stripped. If no
|
|
221
|
+
valid frontmatter block is present, returns ({}, content).
|
|
222
|
+
"""
|
|
223
|
+
lines = content.split("\n")
|
|
224
|
+
end_idx = _frontmatter_end(lines)
|
|
97
225
|
if end_idx is None:
|
|
98
226
|
return {}, content
|
|
99
227
|
|
|
@@ -107,6 +235,90 @@ def split_frontmatter(content):
|
|
|
107
235
|
return fm, body
|
|
108
236
|
|
|
109
237
|
|
|
238
|
+
def _strip_comment(text):
|
|
239
|
+
"""Drop a YAML comment: a `#` at the start or after whitespace, outside quotes."""
|
|
240
|
+
quote = None
|
|
241
|
+
for i, ch in enumerate(text):
|
|
242
|
+
if quote is not None:
|
|
243
|
+
if ch == quote:
|
|
244
|
+
quote = None
|
|
245
|
+
elif ch in "'\"":
|
|
246
|
+
quote = ch
|
|
247
|
+
elif ch == "#" and (i == 0 or text[i - 1].isspace()):
|
|
248
|
+
return text[:i]
|
|
249
|
+
return text
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _unquote(item):
|
|
253
|
+
"""Return a trimmed item without its surrounding pair of quotes, if it has one."""
|
|
254
|
+
item = item.strip()
|
|
255
|
+
if len(item) >= 2 and item[0] == item[-1] and item[0] in "'\"":
|
|
256
|
+
return item[1:-1]
|
|
257
|
+
return item
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def frontmatter_list(content, key):
|
|
261
|
+
"""Return the items of a top-level frontmatter list, or None.
|
|
262
|
+
|
|
263
|
+
Reads the two YAML forms a producer writes for stepsCompleted: a flow
|
|
264
|
+
sequence that starts on the key's line (`key: ['init', 'coverage']`, and
|
|
265
|
+
runs on to its `]`) and a block sequence on the lines below the key
|
|
266
|
+
(`- init`). Items are trimmed, a pair of quotes around one is dropped and
|
|
267
|
+
a trailing comment is ignored; an item holding a comma or a bracket is
|
|
268
|
+
not supported. None when the key is absent, holds a scalar, or opens a
|
|
269
|
+
block sequence with no item. When the key appears more than once, the
|
|
270
|
+
last one counts, as in split_frontmatter().
|
|
271
|
+
"""
|
|
272
|
+
lines = content.split("\n")
|
|
273
|
+
end = _frontmatter_end(lines)
|
|
274
|
+
if end is None:
|
|
275
|
+
return None
|
|
276
|
+
block = lines[1:end]
|
|
277
|
+
items = None
|
|
278
|
+
for idx, line in enumerate(block):
|
|
279
|
+
name, sep, value = line.partition(":")
|
|
280
|
+
if not sep or line[:1].isspace() or name.strip() != key:
|
|
281
|
+
continue
|
|
282
|
+
value = _strip_comment(value).strip()
|
|
283
|
+
if value.startswith("["):
|
|
284
|
+
text, nxt = value, idx + 1
|
|
285
|
+
while "]" not in text and nxt < len(block):
|
|
286
|
+
text += " " + _strip_comment(block[nxt]).strip()
|
|
287
|
+
nxt += 1
|
|
288
|
+
inner = text[1:].split("]", 1)[0]
|
|
289
|
+
items = [_unquote(part) for part in inner.split(",") if part.strip()]
|
|
290
|
+
elif value:
|
|
291
|
+
items = None # a scalar, not a list
|
|
292
|
+
else:
|
|
293
|
+
found = []
|
|
294
|
+
for following in block[idx + 1:]:
|
|
295
|
+
stripped = _strip_comment(following).strip()
|
|
296
|
+
if not stripped:
|
|
297
|
+
continue
|
|
298
|
+
match = _BLOCK_ITEM_RE.match(stripped)
|
|
299
|
+
if match is None:
|
|
300
|
+
break
|
|
301
|
+
found.append(_unquote(match.group(1) or ""))
|
|
302
|
+
items = found or None
|
|
303
|
+
return items
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _whole_percentage(value):
|
|
307
|
+
"""Return a frontmatter percentage as an int from 0 to 100, or None.
|
|
308
|
+
|
|
309
|
+
`value` is the scalar split_frontmatter() read (quotes already dropped); a
|
|
310
|
+
trailing comment is ignored. Anything else, a fraction, a sign, a number
|
|
311
|
+
above 100 or no value at all, gives None.
|
|
312
|
+
"""
|
|
313
|
+
if value is None:
|
|
314
|
+
return None
|
|
315
|
+
value = _strip_comment(value).strip()
|
|
316
|
+
if not _WHOLE_NUMBER_RE.fullmatch(value):
|
|
317
|
+
return None
|
|
318
|
+
number = int(value)
|
|
319
|
+
return number if number <= 100 else None
|
|
320
|
+
|
|
321
|
+
|
|
110
322
|
def scan_headings(body):
|
|
111
323
|
"""Scan the body for the canonical level-2 sections.
|
|
112
324
|
|
|
@@ -150,60 +362,352 @@ def scan_headings(body):
|
|
|
150
362
|
return missing, order_violations
|
|
151
363
|
|
|
152
364
|
|
|
365
|
+
def _uninterpreted():
|
|
366
|
+
"""The keys read_report() fills only for a report whose schemaVersion is "1.0"."""
|
|
367
|
+
return {
|
|
368
|
+
"generatedAt": None,
|
|
369
|
+
"overallVerdict": None,
|
|
370
|
+
"coveragePercentage": None,
|
|
371
|
+
"coverageMeasured": None,
|
|
372
|
+
"verdictTableFound": None,
|
|
373
|
+
"duplicateVerdictTableLine": None,
|
|
374
|
+
"pairVerdicts": [],
|
|
375
|
+
"unknownTokens": [],
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def read_report(content):
|
|
380
|
+
"""Check and read one report's text: the part both modes share.
|
|
381
|
+
|
|
382
|
+
Returns (fields, ok). `fields` holds schemaVersion, schemaVersionOk,
|
|
383
|
+
headingsOk, missingHeadings and orderViolations, plus the keys
|
|
384
|
+
_uninterpreted() lists (see the module docstring for each). The sections
|
|
385
|
+
are checked whatever the version; a report whose schemaVersion is not
|
|
386
|
+
"1.0" is not interpreted, so its other keys keep their _uninterpreted()
|
|
387
|
+
values. `ok` is True when every check passes: the sections present and in
|
|
388
|
+
order, schemaVersion "1.0", the canonical table there once and every
|
|
389
|
+
verdict token known.
|
|
390
|
+
"""
|
|
391
|
+
fm, body = split_frontmatter(content)
|
|
392
|
+
schema_version = fm.get("schemaVersion") # None if the key is absent
|
|
393
|
+
missing, order_violations = scan_headings(body)
|
|
394
|
+
fields = {
|
|
395
|
+
"schemaVersion": schema_version,
|
|
396
|
+
"schemaVersionOk": schema_version == EXPECTED_SCHEMA_VERSION,
|
|
397
|
+
"headingsOk": not missing and not order_violations,
|
|
398
|
+
"missingHeadings": missing,
|
|
399
|
+
"orderViolations": order_violations,
|
|
400
|
+
**_uninterpreted(),
|
|
401
|
+
}
|
|
402
|
+
if not fields["schemaVersionOk"]:
|
|
403
|
+
# An unknown version is never interpreted: nothing is read from it.
|
|
404
|
+
return fields, False
|
|
405
|
+
|
|
406
|
+
overall = fm.get("overallVerdict")
|
|
407
|
+
unknown = []
|
|
408
|
+
if overall not in OVERALL_VERDICTS:
|
|
409
|
+
unknown.append({"field": "overallVerdict", "token": overall})
|
|
410
|
+
|
|
411
|
+
# The body starts on the file line after the frontmatter's closing `---`.
|
|
412
|
+
first_line = len(content.split("\n")) - len(body.split("\n")) + 1
|
|
413
|
+
found, rows, duplicate_line = read_verdict_table(body, first_line)
|
|
414
|
+
pairs = []
|
|
415
|
+
for line_number, cells in rows:
|
|
416
|
+
pair = dict(zip(VERDICT_TABLE_HEADER, cells))
|
|
417
|
+
pairs.append(pair)
|
|
418
|
+
if pair["verdict"] not in PAIR_VERDICTS:
|
|
419
|
+
unknown.append(
|
|
420
|
+
{
|
|
421
|
+
"field": "verdict",
|
|
422
|
+
"line": line_number,
|
|
423
|
+
"lib_a": pair["lib_a"],
|
|
424
|
+
"lib_b": pair["lib_b"],
|
|
425
|
+
"token": pair["verdict"],
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
coverage = _whole_percentage(fm.get("coveragePercentage"))
|
|
430
|
+
steps = frontmatter_list(content, STEPS_COMPLETED_KEY) or []
|
|
431
|
+
fields.update(
|
|
432
|
+
generatedAt=fm.get("generatedAt") or None,
|
|
433
|
+
overallVerdict=overall,
|
|
434
|
+
coveragePercentage=coverage,
|
|
435
|
+
coverageMeasured=coverage is not None and COVERAGE_STEP in steps,
|
|
436
|
+
verdictTableFound=found,
|
|
437
|
+
duplicateVerdictTableLine=duplicate_line,
|
|
438
|
+
pairVerdicts=pairs,
|
|
439
|
+
unknownTokens=unknown,
|
|
440
|
+
)
|
|
441
|
+
ok = fields["headingsOk"] and found and duplicate_line is None and not unknown
|
|
442
|
+
return fields, ok
|
|
443
|
+
|
|
444
|
+
|
|
153
445
|
def validate_report(path):
|
|
154
|
-
"""
|
|
446
|
+
"""Check and read one feasibility report. Returns (result_dict, exit_code)."""
|
|
155
447
|
p = Path(path)
|
|
156
448
|
try:
|
|
157
|
-
|
|
449
|
+
# utf-8-sig drops the byte order mark some editors add, which would
|
|
450
|
+
# otherwise hide the frontmatter's opening `---`.
|
|
451
|
+
content = p.read_text(encoding="utf-8-sig")
|
|
158
452
|
except (OSError, UnicodeDecodeError) as e:
|
|
159
453
|
return (
|
|
160
454
|
{
|
|
161
455
|
"status": "error",
|
|
162
456
|
"path": str(p),
|
|
163
457
|
"error": f"could not read report file: {e}",
|
|
164
|
-
"
|
|
458
|
+
"schemaVersion": None,
|
|
165
459
|
"schemaVersionFound": None,
|
|
460
|
+
# The values earlier callers read for a report that cannot be read.
|
|
461
|
+
"schemaVersionOk": False,
|
|
166
462
|
"headingsOk": False,
|
|
167
463
|
"missingHeadings": [],
|
|
168
464
|
"orderViolations": [],
|
|
465
|
+
**_uninterpreted(),
|
|
169
466
|
"violation": "io-error",
|
|
170
467
|
},
|
|
171
468
|
2,
|
|
172
469
|
)
|
|
173
470
|
|
|
174
|
-
|
|
175
|
-
schema_version = fm.get("schemaVersion") # None if the key is absent
|
|
176
|
-
schema_version_ok = schema_version == EXPECTED_SCHEMA_VERSION
|
|
177
|
-
|
|
178
|
-
missing, order_violations = scan_headings(body)
|
|
179
|
-
headings_ok = not missing and not order_violations
|
|
180
|
-
|
|
181
|
-
ok = schema_version_ok and headings_ok
|
|
471
|
+
fields, ok = read_report(content)
|
|
182
472
|
result = {
|
|
183
473
|
"status": "ok" if ok else "error",
|
|
184
474
|
"path": str(p),
|
|
185
|
-
"
|
|
186
|
-
|
|
187
|
-
"headingsOk": headings_ok,
|
|
188
|
-
"missingHeadings": missing,
|
|
189
|
-
"orderViolations": order_violations,
|
|
475
|
+
"schemaVersionFound": fields["schemaVersion"], # the name earlier callers read
|
|
476
|
+
**fields,
|
|
190
477
|
"violation": None if ok else "schema-violation",
|
|
191
478
|
}
|
|
192
479
|
return result, (0 if ok else 1)
|
|
193
480
|
|
|
194
481
|
|
|
195
|
-
def
|
|
196
|
-
|
|
482
|
+
def slugify(project_name):
|
|
483
|
+
"""Return the `{project_slug}` of a project name (the producer's rule).
|
|
484
|
+
|
|
485
|
+
NFKD-decompose the name and drop its combining marks, so an accented Latin
|
|
486
|
+
letter keeps its base letter (`Café` -> `cafe`) and compatibility forms
|
|
487
|
+
fold to ASCII (full-width letters, ligatures, superscripts). A letter or
|
|
488
|
+
digit that still has no ASCII form is dropped (`Straße` -> `strae`).
|
|
489
|
+
Lowercase the rest, turn every run of other characters (spaces,
|
|
490
|
+
punctuation and symbols of any script) into one hyphen, and trim hyphens
|
|
491
|
+
from both ends. A name that leaves nothing gets FALLBACK_SLUG.
|
|
492
|
+
|
|
493
|
+
Deterministic and idempotent: slugify(slugify(name)) == slugify(name).
|
|
494
|
+
"""
|
|
495
|
+
kept = []
|
|
496
|
+
for ch in unicodedata.normalize("NFKD", project_name):
|
|
497
|
+
if unicodedata.category(ch).startswith("M"):
|
|
498
|
+
continue # a combining mark; the letter it sat on stays
|
|
499
|
+
if ch.isascii() and ch.isalnum():
|
|
500
|
+
kept.append(ch.lower())
|
|
501
|
+
elif ch.isalnum():
|
|
502
|
+
continue # a letter or digit with no ASCII form
|
|
503
|
+
else:
|
|
504
|
+
kept.append("-")
|
|
505
|
+
return _HYPHEN_RUN_RE.sub("-", "".join(kept)).strip("-") or FALLBACK_SLUG
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _table_cells(line):
|
|
509
|
+
"""Split one markdown table row into its trimmed cells.
|
|
510
|
+
|
|
511
|
+
The leading pipe is stripped and so is a trailing one; `\\|` is a pipe
|
|
512
|
+
inside a cell (GFM) and comes back as `|`.
|
|
513
|
+
"""
|
|
514
|
+
row = line.strip()
|
|
515
|
+
if row.startswith("|"):
|
|
516
|
+
row = row[1:]
|
|
517
|
+
if row.endswith("|") and not row.endswith("\\|"):
|
|
518
|
+
row = row[:-1]
|
|
519
|
+
return [cell.strip().replace("\\|", "|") for cell in _CELL_SEPARATOR_RE.split(row)]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def read_verdict_table(body, first_line=1):
|
|
523
|
+
"""Read the canonical pair table under `## Integration Verdicts`.
|
|
524
|
+
|
|
525
|
+
Returns (found, rows, duplicate_line). `found` is True when that section
|
|
526
|
+
holds a table whose header row is exactly VERDICT_TABLE_HEADER, followed
|
|
527
|
+
by a delimiter row. `rows` lists each data row of the first such table as
|
|
528
|
+
(line_number, [lib_a, lib_b, verdict, rationale]), counting lines from
|
|
529
|
+
`first_line`, the file line of the body's first line. Table lines start
|
|
530
|
+
with a pipe, and a table ends at the first line that does not. Fenced
|
|
531
|
+
code and HTML comments (from a line that starts with `<!--` to the first
|
|
532
|
+
line that holds `-->`) are skipped, so an example table in either is not
|
|
533
|
+
read. The display table the producer writes below the canonical one has
|
|
534
|
+
more columns, so its header never matches.
|
|
535
|
+
|
|
536
|
+
The report holds the canonical table once. `duplicate_line` is the file
|
|
537
|
+
line of the header row of a second one, in the same section or in a
|
|
538
|
+
repeated `## Integration Verdicts` section, and None when there is none:
|
|
539
|
+
a producer that appends its table below the template's empty one leaves
|
|
540
|
+
two, and reading only the first would drop every pair.
|
|
541
|
+
|
|
542
|
+
A row with fewer than four cells gets empty ones; a row with more keeps
|
|
543
|
+
the extra cells in its rationale (an unescaped `|` in the text).
|
|
544
|
+
|
|
545
|
+
Deterministic: same body, same result.
|
|
546
|
+
"""
|
|
547
|
+
width = len(VERDICT_TABLE_HEADER)
|
|
548
|
+
lines = body.split("\n")
|
|
549
|
+
found, rows = False, []
|
|
550
|
+
fence = None
|
|
551
|
+
in_comment = False
|
|
552
|
+
in_section = False
|
|
553
|
+
resume = 0 # the line after the first canonical table's last row
|
|
554
|
+
for idx, line in enumerate(lines):
|
|
555
|
+
if idx < resume:
|
|
556
|
+
continue
|
|
557
|
+
fence_match = _FENCE_RE.match(line)
|
|
558
|
+
if fence is not None:
|
|
559
|
+
marker = fence_match.group(1) if fence_match else ""
|
|
560
|
+
if marker[:1] == fence[0] and len(marker) >= len(fence):
|
|
561
|
+
fence = None
|
|
562
|
+
continue
|
|
563
|
+
if in_comment:
|
|
564
|
+
in_comment = _COMMENT_CLOSE not in line
|
|
565
|
+
continue
|
|
566
|
+
if fence_match:
|
|
567
|
+
fence = fence_match.group(1)
|
|
568
|
+
continue
|
|
569
|
+
if _COMMENT_OPEN_RE.match(line):
|
|
570
|
+
in_comment = _COMMENT_CLOSE not in line
|
|
571
|
+
continue
|
|
572
|
+
if _SECTION_END_RE.match(line):
|
|
573
|
+
heading = _H2_RE.match(line)
|
|
574
|
+
in_section = heading is not None and heading.group(1).strip() == VERDICTS_HEADING
|
|
575
|
+
continue
|
|
576
|
+
if not in_section or not line.lstrip().startswith("|"):
|
|
577
|
+
continue
|
|
578
|
+
if _table_cells(line) != VERDICT_TABLE_HEADER:
|
|
579
|
+
continue
|
|
580
|
+
delimiter = lines[idx + 1] if idx + 1 < len(lines) else ""
|
|
581
|
+
delimiter_cells = _table_cells(delimiter)
|
|
582
|
+
if not (
|
|
583
|
+
delimiter.lstrip().startswith("|")
|
|
584
|
+
and len(delimiter_cells) == width
|
|
585
|
+
and all(_DELIMITER_CELL_RE.match(cell) for cell in delimiter_cells)
|
|
586
|
+
):
|
|
587
|
+
continue
|
|
588
|
+
if found:
|
|
589
|
+
return True, rows, first_line + idx
|
|
590
|
+
found = True
|
|
591
|
+
resume = idx + 2
|
|
592
|
+
while resume < len(lines) and lines[resume].lstrip().startswith("|"):
|
|
593
|
+
cells = _table_cells(lines[resume])
|
|
594
|
+
if len(cells) > width:
|
|
595
|
+
cells = cells[: width - 1] + [" | ".join(cells[width - 1:])]
|
|
596
|
+
cells += [""] * (width - len(cells))
|
|
597
|
+
rows.append((first_line + resume, cells))
|
|
598
|
+
resume += 1
|
|
599
|
+
return found, rows, None
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def locate_report(folder, project_name):
|
|
603
|
+
"""Find a project's -latest feasibility report, then check and read it.
|
|
604
|
+
|
|
605
|
+
Returns (result_dict, exit_code): see "Locate mode" in the module
|
|
606
|
+
docstring for the keys and the exit codes. The report found is checked
|
|
607
|
+
and read by read_report(), as validate mode reads a report path.
|
|
608
|
+
"""
|
|
609
|
+
slug = slugify(project_name)
|
|
610
|
+
latest = Path(folder) / LATEST_REPORT_NAME.format(slug=slug)
|
|
611
|
+
result = {
|
|
612
|
+
"status": "not-found",
|
|
613
|
+
"projectName": project_name,
|
|
614
|
+
"projectSlug": slug,
|
|
615
|
+
"latestPath": str(latest),
|
|
616
|
+
"path": None,
|
|
617
|
+
"schemaVersion": None,
|
|
618
|
+
"schemaVersionOk": None,
|
|
619
|
+
"headingsOk": None,
|
|
620
|
+
"missingHeadings": [],
|
|
621
|
+
"orderViolations": [],
|
|
622
|
+
**_uninterpreted(),
|
|
623
|
+
"violation": None,
|
|
624
|
+
}
|
|
625
|
+
try:
|
|
626
|
+
if not latest.is_file():
|
|
627
|
+
return result, 0
|
|
628
|
+
content = latest.read_text(encoding="utf-8-sig") # see validate_report
|
|
629
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
630
|
+
result.update(
|
|
631
|
+
status="error",
|
|
632
|
+
path=str(latest),
|
|
633
|
+
violation="io-error",
|
|
634
|
+
error=f"could not read report file: {e}",
|
|
635
|
+
)
|
|
636
|
+
return result, 2
|
|
637
|
+
|
|
638
|
+
fields, ok = read_report(content)
|
|
639
|
+
result.update(fields)
|
|
640
|
+
result.update(
|
|
641
|
+
status="ok" if ok else "error",
|
|
642
|
+
path=str(latest),
|
|
643
|
+
violation=None if ok else "schema-violation",
|
|
644
|
+
)
|
|
645
|
+
return result, (0 if ok else 1)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class _PairedFlagsParser(argparse.ArgumentParser):
|
|
649
|
+
"""Check the flags that only work together as part of parsing.
|
|
650
|
+
|
|
651
|
+
The checks run in parse_known_args, which parse_args calls, so a caller
|
|
652
|
+
that only parses with the parser _build_parser() returns (the helper-call
|
|
653
|
+
contract test does) refuses `--locate` without `--project-name` as main()
|
|
654
|
+
does.
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
def parse_known_args(self, args=None, namespace=None):
|
|
658
|
+
parsed, extras = super().parse_known_args(args, namespace)
|
|
659
|
+
if parsed.locate is not None:
|
|
660
|
+
if parsed.project_name is None:
|
|
661
|
+
self.error("--project-name is required with --locate")
|
|
662
|
+
if not parsed.locate.strip():
|
|
663
|
+
self.error("--locate needs a folder path")
|
|
664
|
+
elif parsed.project_name is not None:
|
|
665
|
+
self.error("--project-name only applies with --locate")
|
|
666
|
+
return parsed, extras
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _build_parser():
|
|
670
|
+
parser = _PairedFlagsParser(
|
|
197
671
|
description=(
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"Coverage Analysis, Integration Verdicts,
|
|
201
|
-
"Evidence Sources) are present and in canonical
|
|
202
|
-
"frontmatter schemaVersion == \"1.0\"
|
|
203
|
-
"
|
|
672
|
+
"Check and read a feasibility report, given its path or found with "
|
|
673
|
+
"--locate. Both modes confirm the five required body sections "
|
|
674
|
+
"(Executive Summary, Coverage Analysis, Integration Verdicts, "
|
|
675
|
+
"Recommendations, Evidence Sources) are present and in canonical "
|
|
676
|
+
"order and that frontmatter schemaVersion == \"1.0\"; for a 1.0 "
|
|
677
|
+
"report they also require the one canonical Integration Verdicts "
|
|
678
|
+
"table and known verdict tokens, and return generatedAt, "
|
|
679
|
+
"overallVerdict, coveragePercentage, coverageMeasured, "
|
|
680
|
+
"pairVerdicts and unknownTokens. Given a report path: exit 0 "
|
|
681
|
+
"valid, 1 schema-violation, 2 IO or usage error. Given --locate "
|
|
682
|
+
"FOLDER --project-name NAME: reads "
|
|
683
|
+
"FOLDER/feasibility-report-<slug>-latest.md, the slug made from "
|
|
684
|
+
"NAME by the producer's rule; exit 0 ok or not-found, 1 "
|
|
685
|
+
"schema-violation, 2 IO or usage error."
|
|
204
686
|
)
|
|
205
687
|
)
|
|
206
|
-
parser.
|
|
688
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
689
|
+
target.add_argument(
|
|
690
|
+
"report",
|
|
691
|
+
nargs="?",
|
|
692
|
+
help="path to the feasibility report .md file to check and read",
|
|
693
|
+
)
|
|
694
|
+
target.add_argument(
|
|
695
|
+
"--locate",
|
|
696
|
+
metavar="FOLDER",
|
|
697
|
+
help=(
|
|
698
|
+
"find and read the project's feasibility-report-<slug>-latest.md in "
|
|
699
|
+
"FOLDER (the forge data folder); needs --project-name"
|
|
700
|
+
),
|
|
701
|
+
)
|
|
702
|
+
parser.add_argument(
|
|
703
|
+
"--project-name",
|
|
704
|
+
metavar="NAME",
|
|
705
|
+
help=(
|
|
706
|
+
"the project_name the report was written for (with --locate); a "
|
|
707
|
+
"name that leaves no slug, an empty one included, gets the slug "
|
|
708
|
+
"'project'"
|
|
709
|
+
),
|
|
710
|
+
)
|
|
207
711
|
parser.add_argument(
|
|
208
712
|
"-o",
|
|
209
713
|
"--output",
|
|
@@ -214,9 +718,16 @@ def main(argv=None):
|
|
|
214
718
|
action="store_true",
|
|
215
719
|
help="print the resolved exit code to stderr",
|
|
216
720
|
)
|
|
217
|
-
|
|
721
|
+
return parser
|
|
722
|
+
|
|
218
723
|
|
|
219
|
-
|
|
724
|
+
def main(argv=None):
|
|
725
|
+
args = _build_parser().parse_args(argv)
|
|
726
|
+
|
|
727
|
+
if args.locate is not None:
|
|
728
|
+
result, exit_code = locate_report(args.locate, args.project_name)
|
|
729
|
+
else:
|
|
730
|
+
result, exit_code = validate_report(args.report)
|
|
220
731
|
text = json.dumps(result, indent=2)
|
|
221
732
|
|
|
222
733
|
if args.output:
|