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
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = ["pyyaml"]
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF test-skill coverage inputs: the counts, name sets and candidates coverage-check reads.
|
|
7
|
+
|
|
8
|
+
coverage-check.md decides which denominator clause applies to a skill and
|
|
9
|
+
whether a bookkeeping variant is a real export; everything it counts, folds
|
|
10
|
+
or unions on the way runs here, so no count is made by hand. Three
|
|
11
|
+
subcommands, each reading files and writing one JSON object:
|
|
12
|
+
|
|
13
|
+
census docs-only detection. Counts the citations SKILL.md and each .md
|
|
14
|
+
file directly in references/ write, by form: `[EXT:` against
|
|
15
|
+
the local forms `[AST:`, `[SRC:` and a stack's `[from skill:`
|
|
16
|
+
(`[QMD:` and `[DOC:` are counted and decide nothing). A skill is
|
|
17
|
+
docs-only when it cites `[EXT:` and no local form.
|
|
18
|
+
|
|
19
|
+
metadata the metadata.json and provenance-map counts. Cluster A
|
|
20
|
+
(`stats.exports_public_api`, the `exports[]` length) and Cluster
|
|
21
|
+
B (`stats.exports_documented`, the provenance entry names, the
|
|
22
|
+
`confidence_distribution`) under the keys
|
|
23
|
+
check-metadata-coherence.py reads; the provenance named exports
|
|
24
|
+
(`::` impl-block methods left out); the inflation signature
|
|
25
|
+
(`stats.exports_documented` equals `stats.effective_denominator`)
|
|
26
|
+
with the declared names verify-declared-numerator.py looks up
|
|
27
|
+
(the `exports[]` names, else the provenance named exports); and,
|
|
28
|
+
for a stack, its composition surface: the distinct provenance
|
|
29
|
+
named exports when the map has entries, else each library and
|
|
30
|
+
each integration pair (`{a} + {b}`, as the stack's Library Pair
|
|
31
|
+
Integrations heading writes it), as reconcile-coverage.py's
|
|
32
|
+
stack branch takes them.
|
|
33
|
+
|
|
34
|
+
surface the enumerated source surface, from every input given (their
|
|
35
|
+
union):
|
|
36
|
+
--extraction skf-extract-public-api.py --mode full output:
|
|
37
|
+
the names in entry_point_diff.public, less the
|
|
38
|
+
outside_scope names (defined in a file the brief
|
|
39
|
+
scopes out); exports[] only gives each name's
|
|
40
|
+
kind, line and signature line, looked up by the
|
|
41
|
+
name its defining file gives it (an aliased
|
|
42
|
+
re-export's `local`) and that file.
|
|
43
|
+
extraction_gaps are listed to read by eye.
|
|
44
|
+
--quick skf-extract-public-api.py --mode quick output
|
|
45
|
+
--per-file a subagent's per-file result (exports_found and
|
|
46
|
+
types_found), schema-checked as
|
|
47
|
+
score-signatures.py checks it
|
|
48
|
+
--name a name read by eye whose file is not known
|
|
49
|
+
--provenance the provenance entries' named exports (State 2)
|
|
50
|
+
--metadata the metadata.json exports[] names (States 2, 3)
|
|
51
|
+
A surface read from the source (--extraction, --quick or
|
|
52
|
+
--per-file) takes no name from --provenance and --metadata:
|
|
53
|
+
they are the baselines of the candidates and guards. Without
|
|
54
|
+
one, with --provenance and --metadata both, `state2` gives the
|
|
55
|
+
provenance count, the metadata count and their union count,
|
|
56
|
+
which compute-score.py turns into the State 2 divergence. --fold
|
|
57
|
+
folds the bookkeeping variants among the provenance and metadata
|
|
58
|
+
names to their base name (`X_def`, `X_exact`, `a11y_X`, and
|
|
59
|
+
`<prefix>X` for each --fold-prefix when `X` is itself a name)
|
|
60
|
+
unless --keep names the variant as a real export, and
|
|
61
|
+
`canonical` gives the provenance map's fold summary. --brief
|
|
62
|
+
gives the skill brief's scope and language; an extraction's
|
|
63
|
+
recorded scope wins over it.
|
|
64
|
+
|
|
65
|
+
Name sets (surface `sets`, each sorted): `all` (the union above);
|
|
66
|
+
`scope.include` and `tier_a_include` when the scope has those globs and a
|
|
67
|
+
record names a file: the names whose file (an extraction's definition file,
|
|
68
|
+
the quick output's source_file, a per-file result's file, a provenance
|
|
69
|
+
entry's source_file) matches a glob of the list and no scope.exclude glob,
|
|
70
|
+
so a name with no file is in `all` only; and, from an extraction only,
|
|
71
|
+
`subpaths` (the names a non-root `exports` map subpath without `*` reaches)
|
|
72
|
+
and `root` (the names the root entry points reach). reconcile-coverage.py
|
|
73
|
+
and score-signatures.py --surface-set pick one.
|
|
74
|
+
|
|
75
|
+
Candidates and guards (with an extraction, --brief or --metadata): each
|
|
76
|
+
set's count, the metadata `stats.effective_denominator` (--metadata), and
|
|
77
|
+
the extractor's own effective_denominator and its basis. Both guards run
|
|
78
|
+
only on a surface read from the source. The deflation guard compares the
|
|
79
|
+
re-derived scope.include set (the `all` set without one) with
|
|
80
|
+
`stats.effective_denominator` and fires above DEFLATION_PCT when the scope
|
|
81
|
+
has no tier_a_include; the inflation guard compares the scope.include set
|
|
82
|
+
with the provenance entry count (--provenance) and fires above
|
|
83
|
+
INFLATION_PCT when the scope has no tier_a_include; the umbrella ratio (an
|
|
84
|
+
extraction only) is the share of the root entry points' names that are
|
|
85
|
+
re-exports, and above UMBRELLA_RATIO the barrel is an umbrella. Globs follow
|
|
86
|
+
skf-extract-public-api.py: `**` spans any number of path segments, none
|
|
87
|
+
included, and `*` and `?` stay inside one.
|
|
88
|
+
|
|
89
|
+
Fallback verdict: `extraction.fallback.needed` is true when the extractor
|
|
90
|
+
found no ast-grep (`no-ast-grep`), read no file in scope, read files only
|
|
91
|
+
in languages it has no recipe for (`files_without_recipes` and no export),
|
|
92
|
+
or found files no recipe reads while the skill's language (the --brief
|
|
93
|
+
`language`, else the metadata.json `language`) is not one RECIPE_LANGUAGES
|
|
94
|
+
holds, so the per-file scan has to run instead. An `incomplete` run keeps
|
|
95
|
+
its exports; `extraction.readByEye` lists the files its errors and file
|
|
96
|
+
issues name.
|
|
97
|
+
|
|
98
|
+
Usage:
|
|
99
|
+
uv run load-coverage-inputs.py census --skill-dir <dir> [--output <file>]
|
|
100
|
+
uv run load-coverage-inputs.py metadata --metadata <metadata.json> \\
|
|
101
|
+
[--provenance <provenance-map.json>] [--output <file>]
|
|
102
|
+
uv run load-coverage-inputs.py surface [--extraction <file>] [--quick <file>]... \\
|
|
103
|
+
[--per-file <file>]... [--name <name>]... [--provenance <file>] [--metadata <file>] \\
|
|
104
|
+
[--brief <skill-brief.yaml>] [--fold] [--fold-prefix <prefix>]... [--keep <name>]... \\
|
|
105
|
+
[--output <file>]
|
|
106
|
+
|
|
107
|
+
Exit codes:
|
|
108
|
+
0 the result was printed (and written to --output)
|
|
109
|
+
1 an input file is missing, unreadable or not the JSON (or the YAML
|
|
110
|
+
brief) it should be (one line on stderr)
|
|
111
|
+
2 a per-file result breaks the schema: {"valid": false, "violations":
|
|
112
|
+
[...]} names each breach and nothing is written to --output; or a
|
|
113
|
+
usage error (argparse)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
from __future__ import annotations
|
|
117
|
+
|
|
118
|
+
import argparse
|
|
119
|
+
import importlib.util
|
|
120
|
+
import json
|
|
121
|
+
import math
|
|
122
|
+
import re
|
|
123
|
+
import sys
|
|
124
|
+
from pathlib import Path
|
|
125
|
+
|
|
126
|
+
DEFLATION_PCT = 25
|
|
127
|
+
INFLATION_PCT = 25
|
|
128
|
+
UMBRELLA_RATIO = 0.5
|
|
129
|
+
METHOD_SEPARATOR = "::"
|
|
130
|
+
CITATION_FORMS = {
|
|
131
|
+
"EXT": "[EXT:",
|
|
132
|
+
"AST": "[AST:",
|
|
133
|
+
"SRC": "[SRC:",
|
|
134
|
+
"from skill": "[from skill:",
|
|
135
|
+
"QMD": "[QMD:",
|
|
136
|
+
"DOC": "[DOC:",
|
|
137
|
+
}
|
|
138
|
+
LOCAL_FORMS = ("AST", "SRC", "from skill")
|
|
139
|
+
# Bookkeeping variants folded to their base name, in rule order.
|
|
140
|
+
FOLD_SUFFIXES = ("_def", "_exact")
|
|
141
|
+
FOLD_PREFIX = "a11y_"
|
|
142
|
+
ROOT_SUBPATHS = (None, ".")
|
|
143
|
+
# The languages skf-extract-public-api.py's recipes read (its LANGUAGE_FAMILIES).
|
|
144
|
+
RECIPE_LANGUAGES = frozenset({"python", "rust", "go", "golang", "typescript", "ts", "tsx", "javascript", "js",
|
|
145
|
+
"jsx", "vue"})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class InputError(Exception):
|
|
149
|
+
"""An input file that cannot be used."""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SchemaError(Exception):
|
|
153
|
+
"""Per-file results that break the schema: `violations` names each breach."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, violations: list[str]):
|
|
156
|
+
super().__init__("; ".join(violations))
|
|
157
|
+
self.violations = violations
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
_SIBLINGS: dict[str, object] = {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _sibling(filename: str):
|
|
164
|
+
"""A script beside this one, loaded once: validate-inventory.py (the doc
|
|
165
|
+
text) and score-signatures.py (the per-file result schema)."""
|
|
166
|
+
module = _SIBLINGS.get(filename)
|
|
167
|
+
if module is None:
|
|
168
|
+
path = Path(__file__).resolve().parent / filename
|
|
169
|
+
spec = importlib.util.spec_from_file_location("skf_" + filename[:-3].replace("-", "_"), path)
|
|
170
|
+
if spec is None or spec.loader is None or not path.is_file():
|
|
171
|
+
raise InputError(f"{filename} not found beside {Path(__file__).name}")
|
|
172
|
+
module = importlib.util.module_from_spec(spec)
|
|
173
|
+
spec.loader.exec_module(module)
|
|
174
|
+
_SIBLINGS[filename] = module
|
|
175
|
+
return module
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def round2(value: float) -> float:
|
|
179
|
+
"""Round to 2 decimals with JS-compatible half-up rounding (matches compute-score.py)."""
|
|
180
|
+
return math.floor(value * 100 + 0.5) / 100
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _read_json(path: str, flag: str):
|
|
184
|
+
try:
|
|
185
|
+
return json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
186
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
187
|
+
raise InputError(f"cannot read {flag} {path}: {exc}") from exc
|
|
188
|
+
except json.JSONDecodeError as exc:
|
|
189
|
+
raise InputError(f"{flag} {path} is not JSON: {exc.msg}") from exc
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _object(path: str, flag: str) -> dict:
|
|
193
|
+
data = _read_json(path, flag)
|
|
194
|
+
if not isinstance(data, dict):
|
|
195
|
+
raise InputError(f"{flag} {path} is not a JSON object")
|
|
196
|
+
return data
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _count(value) -> int | None:
|
|
200
|
+
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _export_name(item) -> str | None:
|
|
204
|
+
"""The name of a metadata exports[] item: a string, or an object's name."""
|
|
205
|
+
if isinstance(item, str):
|
|
206
|
+
return item or None
|
|
207
|
+
if isinstance(item, dict):
|
|
208
|
+
for key in ("name", "export_name"):
|
|
209
|
+
if isinstance(item.get(key), str) and item[key]:
|
|
210
|
+
return item[key]
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _entries(provenance: dict | None) -> list[dict]:
|
|
215
|
+
entries = provenance.get("entries") if isinstance(provenance, dict) else None
|
|
216
|
+
return [e for e in entries if isinstance(e, dict)] if isinstance(entries, list) else []
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _entry_names(provenance: dict | None) -> list[str]:
|
|
220
|
+
return [e["export_name"] for e in _entries(provenance)
|
|
221
|
+
if isinstance(e.get("export_name"), str) and e["export_name"]]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# --------------------------------------------------------------------------
|
|
225
|
+
# census
|
|
226
|
+
# --------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def census(skill_dir: str) -> dict:
|
|
230
|
+
root = Path(skill_dir)
|
|
231
|
+
if not (root / "SKILL.md").is_file():
|
|
232
|
+
raise InputError(f"--skill-dir {skill_dir} holds no SKILL.md")
|
|
233
|
+
try:
|
|
234
|
+
text = _sibling("validate-inventory.py").load_doc_text(skill_dir)
|
|
235
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
236
|
+
raise InputError(f"cannot read the skill text under {skill_dir}: {exc}") from exc
|
|
237
|
+
counts = {form: text.count(marker) for form, marker in CITATION_FORMS.items()}
|
|
238
|
+
local = sum(counts[form] for form in LOCAL_FORMS)
|
|
239
|
+
refs = root / "references"
|
|
240
|
+
files = ["SKILL.md"] + ([f"references/{p.name}" for p in sorted(refs.glob("*.md"))] if refs.is_dir() else [])
|
|
241
|
+
return {
|
|
242
|
+
"citations": counts,
|
|
243
|
+
"localCitations": local,
|
|
244
|
+
"docsOnly": counts["EXT"] > 0 and local == 0,
|
|
245
|
+
"files": files,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --------------------------------------------------------------------------
|
|
250
|
+
# metadata
|
|
251
|
+
# --------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _pair_name(pair) -> str | None:
|
|
255
|
+
if isinstance(pair, str):
|
|
256
|
+
return pair or None
|
|
257
|
+
if isinstance(pair, (list, tuple)) and len(pair) == 2 and all(isinstance(p, str) and p for p in pair):
|
|
258
|
+
return f"{pair[0]} + {pair[1]}"
|
|
259
|
+
if isinstance(pair, dict) and all(isinstance(pair.get(k), str) and pair[k] for k in ("a", "b")):
|
|
260
|
+
return f"{pair['a']} + {pair['b']}"
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _library_name(lib) -> str | None:
|
|
265
|
+
if isinstance(lib, str):
|
|
266
|
+
return lib or None
|
|
267
|
+
if isinstance(lib, dict) and isinstance(lib.get("name"), str) and lib["name"]:
|
|
268
|
+
return lib["name"]
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def metadata_inputs(meta: dict, provenance: dict | None) -> dict:
|
|
273
|
+
stats = meta.get("stats") if isinstance(meta.get("stats"), dict) else {}
|
|
274
|
+
exports = meta.get("exports")
|
|
275
|
+
names = [n for n in (_export_name(e) for e in exports) if n] if isinstance(exports, list) else []
|
|
276
|
+
entry_names = _entry_names(provenance) if provenance is not None else None
|
|
277
|
+
named = sorted({n for n in entry_names or [] if METHOD_SEPARATOR not in n})
|
|
278
|
+
dist = stats.get("confidence_distribution")
|
|
279
|
+
if not isinstance(dist, dict):
|
|
280
|
+
dist = meta.get("confidence_distribution") if isinstance(meta.get("confidence_distribution"), dict) else None
|
|
281
|
+
if dist is not None:
|
|
282
|
+
dist = {tier: dist[tier] for tier in ("t1", "t1_low", "t2", "t3") if _count(dist.get(tier)) is not None}
|
|
283
|
+
documented = _count(stats.get("exports_documented"))
|
|
284
|
+
effective = _count(stats.get("effective_denominator"))
|
|
285
|
+
skill_type = meta.get("skill_type") if isinstance(meta.get("skill_type"), str) else None
|
|
286
|
+
|
|
287
|
+
stack = None
|
|
288
|
+
if skill_type == "stack":
|
|
289
|
+
if named:
|
|
290
|
+
stack = {"basis": "provenance", "denominator": len(named), "compositionNames": named}
|
|
291
|
+
else:
|
|
292
|
+
libraries = [n for n in (_library_name(lib) for lib in meta.get("libraries") or []) if n]
|
|
293
|
+
pairs = [n for n in (_pair_name(p) for p in meta.get("integration_pairs") or []) if n]
|
|
294
|
+
stack = {"basis": "composition", "denominator": len(libraries) + len(pairs),
|
|
295
|
+
"compositionNames": libraries + pairs}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
"skillType": skill_type,
|
|
299
|
+
"scopeType": meta.get("scope_type") if isinstance(meta.get("scope_type"), str) else None,
|
|
300
|
+
"clusterA": {
|
|
301
|
+
"exports_public_api": _count(stats.get("exports_public_api")),
|
|
302
|
+
"exports_length": len(exports) if isinstance(exports, list) else None,
|
|
303
|
+
},
|
|
304
|
+
"clusterB": {"exports_documented": documented},
|
|
305
|
+
"provenanceExportNames": entry_names,
|
|
306
|
+
"confidenceDistribution": dist or None,
|
|
307
|
+
"namedExports": {"count": len(named), "names": named} if entry_names is not None else None,
|
|
308
|
+
"effectiveDenominator": effective,
|
|
309
|
+
"inflationSignature": documented is not None and effective is not None and documented == effective,
|
|
310
|
+
"declaredNames": sorted(set(names)) if names else named,
|
|
311
|
+
"stack": stack,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
# surface
|
|
317
|
+
# --------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
_GLOB_CACHE: dict[str, re.Pattern[str]] = {}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def glob_match(rel_path: str, pattern: str) -> bool:
|
|
324
|
+
"""skf-resolve-authoritative-files.py's glob rule, which the extractor applies."""
|
|
325
|
+
compiled = _GLOB_CACHE.get(pattern)
|
|
326
|
+
if compiled is None:
|
|
327
|
+
parts = pattern.split("/")
|
|
328
|
+
out: list[str] = []
|
|
329
|
+
for i, part in enumerate(parts):
|
|
330
|
+
if part == "**":
|
|
331
|
+
out.append("(?:.*/)?" if i + 1 < len(parts) else ".*")
|
|
332
|
+
continue
|
|
333
|
+
out.append("".join("[^/]*" if ch == "*" else "[^/]" if ch == "?" else re.escape(ch) for ch in part))
|
|
334
|
+
if i + 1 < len(parts):
|
|
335
|
+
out.append("/")
|
|
336
|
+
compiled = re.compile("^" + "".join(out) + "$")
|
|
337
|
+
_GLOB_CACHE[pattern] = compiled
|
|
338
|
+
return bool(compiled.match(rel_path))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _norm(path) -> str:
|
|
342
|
+
text = str(path or "").strip().replace("\\", "/")
|
|
343
|
+
while text.startswith("./"):
|
|
344
|
+
text = text[2:]
|
|
345
|
+
return text.lstrip("/")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _record(name, kind, file, line, signature_line, origin) -> dict:
|
|
349
|
+
return {"name": name, "kind": kind, "file": file, "line": line, "signatureLine": signature_line,
|
|
350
|
+
"origin": origin}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _globs(value) -> list[str]:
|
|
354
|
+
return [g for g in (_norm(v) for v in value if isinstance(v, str)) if g] if isinstance(value, list) else []
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _scope(include, exclude, tier_a_include) -> dict:
|
|
358
|
+
return {"include": _globs(include), "exclude": _globs(exclude), "tier_a_include": _globs(tier_a_include)}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def read_brief(path: str) -> dict:
|
|
362
|
+
"""A skill brief's scope globs and language, the fields skf-extract-public-api.py reads."""
|
|
363
|
+
try:
|
|
364
|
+
import yaml # --brief only; `uv run` installs it from the header
|
|
365
|
+
except ImportError as exc:
|
|
366
|
+
raise InputError("--brief is read with PyYAML: run the script with `uv run`") from exc
|
|
367
|
+
try:
|
|
368
|
+
data = yaml.safe_load(Path(path).read_bytes().decode("utf-8-sig"))
|
|
369
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
370
|
+
raise InputError(f"cannot read --brief {path}: {exc}") from exc
|
|
371
|
+
except yaml.YAMLError as exc:
|
|
372
|
+
raise InputError(f"--brief {path} is not valid YAML: {' '.join(str(exc).split())}") from exc
|
|
373
|
+
if not isinstance(data, dict):
|
|
374
|
+
raise InputError(f"--brief {path} is not a YAML mapping")
|
|
375
|
+
scope = data.get("scope") if isinstance(data.get("scope"), dict) else {}
|
|
376
|
+
return {"scope": _scope(scope.get("include"), scope.get("exclude"), scope.get("tier_a_include")),
|
|
377
|
+
"language": data.get("language") if isinstance(data.get("language"), str) else None}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def matching(records: list[dict], globs: list[str], excludes: list[str]) -> list[str]:
|
|
381
|
+
"""The names whose record names a file one of `globs` matches and no exclude glob does."""
|
|
382
|
+
return sorted({r["name"] for r in records if r.get("file")
|
|
383
|
+
and any(glob_match(_norm(r["file"]), g) for g in globs)
|
|
384
|
+
and not any(glob_match(_norm(r["file"]), g) for g in excludes)})
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def from_extraction(data: dict, language: str | None = None) -> dict:
|
|
388
|
+
"""The public surface of an extraction, less what lies outside scope."""
|
|
389
|
+
if data.get("mode") != "full":
|
|
390
|
+
raise InputError("--extraction is not skf-extract-public-api.py --mode full output")
|
|
391
|
+
diff = data.get("entry_point_diff") if isinstance(data.get("entry_point_diff"), dict) else {}
|
|
392
|
+
public = [p for p in diff.get("public") or [] if isinstance(p, dict) and isinstance(p.get("name"), str)]
|
|
393
|
+
outside = [o for o in diff.get("outside_scope") or [] if isinstance(o, dict)]
|
|
394
|
+
gaps = [g for g in diff.get("extraction_gaps") or [] if isinstance(g, dict)]
|
|
395
|
+
outside_keys = {(o.get("name"), o.get("file")) for o in outside}
|
|
396
|
+
index = {(e.get("export_name"), e.get("source_file")): e
|
|
397
|
+
for e in data.get("exports") or [] if isinstance(e, dict)}
|
|
398
|
+
records = []
|
|
399
|
+
for info in public:
|
|
400
|
+
name, file = info["name"], info.get("file")
|
|
401
|
+
if (name, file) in outside_keys:
|
|
402
|
+
continue
|
|
403
|
+
# An aliased re-export (`export { Options as Config }`) is recorded
|
|
404
|
+
# under the name its defining file gives it, `local`.
|
|
405
|
+
found = index.get((info.get("local") or name, file)) or index.get((name, file))
|
|
406
|
+
kind = found.get("export_type") if found else None
|
|
407
|
+
records.append(_record(name, None if kind == "re-export" else kind, file,
|
|
408
|
+
found.get("source_line") if found else info.get("line"),
|
|
409
|
+
found.get("signature_line") if found else None, "extraction"))
|
|
410
|
+
|
|
411
|
+
scope = data.get("scope") if isinstance(data.get("scope"), dict) else {}
|
|
412
|
+
|
|
413
|
+
entry_points = data.get("entry_points") if isinstance(data.get("entry_points"), dict) else {}
|
|
414
|
+
entry_files = [e for e in entry_points.get("files") or [] if isinstance(e, dict)]
|
|
415
|
+
subpath_files = {e.get("file") for e in entry_files
|
|
416
|
+
if e.get("subpath") not in ROOT_SUBPATHS and "*" not in str(e.get("subpath"))}
|
|
417
|
+
root_files = {e.get("file") for e in entry_files if e.get("subpath") in ROOT_SUBPATHS}
|
|
418
|
+
root_names = [p for p in public if p.get("entry") in root_files]
|
|
419
|
+
reexported = [p for p in root_names if p.get("via") not in (None, "declaration")]
|
|
420
|
+
|
|
421
|
+
sets = {}
|
|
422
|
+
if subpath_files:
|
|
423
|
+
sets["subpaths"] = sorted({p["name"] for p in public if p.get("entry") in subpath_files})
|
|
424
|
+
if root_files:
|
|
425
|
+
sets["root"] = sorted({p["name"] for p in root_names})
|
|
426
|
+
|
|
427
|
+
counts = data.get("counts") if isinstance(data.get("counts"), dict) else {}
|
|
428
|
+
arms = data.get("arms") if isinstance(data.get("arms"), dict) else {}
|
|
429
|
+
without = data.get("files_without_recipes") if isinstance(data.get("files_without_recipes"), dict) else {}
|
|
430
|
+
status = data.get("status")
|
|
431
|
+
reason = None
|
|
432
|
+
if status == "no-ast-grep":
|
|
433
|
+
reason = "no ast-grep the runner can run"
|
|
434
|
+
elif not data.get("files_in_scope"):
|
|
435
|
+
reason = "no file in scope"
|
|
436
|
+
elif without and not data.get("exports"):
|
|
437
|
+
reason = f"no recipe reads the files in scope ({', '.join(sorted(without))})"
|
|
438
|
+
elif without and language and language.strip().lower() not in RECIPE_LANGUAGES:
|
|
439
|
+
reason = (f"no recipe reads the skill's language, {language} "
|
|
440
|
+
f"({', '.join(sorted(without))} in scope)")
|
|
441
|
+
read_by_eye = sorted({str(i["file"]) for i in data.get("file_issues") or []
|
|
442
|
+
if isinstance(i, dict) and i.get("file") and i.get("issue") != "no-recipes"}
|
|
443
|
+
| {str(e["first_file"]) for e in data.get("errors") or []
|
|
444
|
+
if isinstance(e, dict) and e.get("first_file")})
|
|
445
|
+
return {
|
|
446
|
+
"records": records,
|
|
447
|
+
"sets": sets,
|
|
448
|
+
"outside": [{"name": o.get("name"), "file": o.get("file")} for o in outside],
|
|
449
|
+
"gaps": [{"name": g.get("name"), "file": g.get("file"), "line": g.get("line")} for g in gaps],
|
|
450
|
+
"scope": _scope(scope.get("include"), scope.get("exclude"), scope.get("tier_a_include")),
|
|
451
|
+
"umbrella": {
|
|
452
|
+
"entries": sorted(f for f in root_files if f),
|
|
453
|
+
"names": len(root_names),
|
|
454
|
+
"reexported": len(reexported),
|
|
455
|
+
"ratio": round2(len(reexported) / len(root_names)) if root_names else None,
|
|
456
|
+
"umbrella": bool(root_names) and len(reexported) / len(root_names) > UMBRELLA_RATIO,
|
|
457
|
+
},
|
|
458
|
+
"extraction": {
|
|
459
|
+
"status": status,
|
|
460
|
+
"filesInScope": data.get("files_in_scope"),
|
|
461
|
+
"truncated": data.get("truncated"),
|
|
462
|
+
"effectiveDenominator": _count(counts.get("effective_denominator")),
|
|
463
|
+
"effectiveDenominatorBasis": counts.get("effective_denominator_basis"),
|
|
464
|
+
"exportsPublicApi": _count(counts.get("exports_public_api")),
|
|
465
|
+
"arms": arms,
|
|
466
|
+
"readByEye": read_by_eye,
|
|
467
|
+
"fallback": {"needed": reason is not None, "reason": reason},
|
|
468
|
+
},
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def from_quick(data: dict) -> list[dict]:
|
|
473
|
+
exports = data.get("exports")
|
|
474
|
+
if not isinstance(exports, list):
|
|
475
|
+
raise InputError("--quick is not skf-extract-public-api.py --mode quick output (no exports list)")
|
|
476
|
+
return [_record(e["name"], e.get("type"), e.get("source_file"), None, None, "quick")
|
|
477
|
+
for e in exports if isinstance(e, dict) and isinstance(e.get("name"), str) and e["name"]]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def from_per_file(path: str) -> tuple[list[dict], list[str]]:
|
|
481
|
+
"""(the records of one per-file result, its schema breaches)."""
|
|
482
|
+
scorer = _sibling("score-signatures.py")
|
|
483
|
+
try:
|
|
484
|
+
text = Path(path).read_bytes().decode("utf-8-sig")
|
|
485
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
486
|
+
raise InputError(f"cannot read --per-file {path}: {exc}") from exc
|
|
487
|
+
results, errors = scorer.parse_result_text(text, Path(path).name)
|
|
488
|
+
if errors:
|
|
489
|
+
return [], errors
|
|
490
|
+
records = []
|
|
491
|
+
for result in results:
|
|
492
|
+
types = set(result.get("types_found") or [])
|
|
493
|
+
for name in sorted(set(result.get("exports_found") or []) | types):
|
|
494
|
+
if name:
|
|
495
|
+
records.append(_record(name, "type" if name in types else None, result["file"], None, None,
|
|
496
|
+
"per-file"))
|
|
497
|
+
return records, []
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def fold_names(names: list[str], prefixes: list[str], keep: set[str],
|
|
501
|
+
tallied: set[str] | None = None) -> tuple[dict[str, str], dict]:
|
|
502
|
+
"""({variant: base}, the fold tally of `tallied`, else of every name) under the canonicalization rules."""
|
|
503
|
+
present = set(names)
|
|
504
|
+
variants: dict[str, str] = {}
|
|
505
|
+
tally = {"_def": 0, "_exact": 0, FOLD_PREFIX: 0, "other": 0}
|
|
506
|
+
for name in sorted(present):
|
|
507
|
+
if name in keep:
|
|
508
|
+
continue
|
|
509
|
+
rule, base = None, None
|
|
510
|
+
for suffix in FOLD_SUFFIXES:
|
|
511
|
+
if name.endswith(suffix) and len(name) > len(suffix):
|
|
512
|
+
rule, base = suffix, name[: -len(suffix)]
|
|
513
|
+
break
|
|
514
|
+
if rule is None and name.startswith(FOLD_PREFIX) and len(name) > len(FOLD_PREFIX):
|
|
515
|
+
rule, base = FOLD_PREFIX, name[len(FOLD_PREFIX):]
|
|
516
|
+
if rule is None:
|
|
517
|
+
for prefix in prefixes:
|
|
518
|
+
if prefix and name.startswith(prefix) and name[len(prefix):] in present:
|
|
519
|
+
rule, base = "other", name[len(prefix):]
|
|
520
|
+
break
|
|
521
|
+
if rule is not None:
|
|
522
|
+
variants[name] = base
|
|
523
|
+
if tallied is None or name in tallied:
|
|
524
|
+
tally[rule] += 1
|
|
525
|
+
return variants, tally
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
NO_UMBRELLA = {"entries": [], "names": 0, "reexported": 0, "ratio": None, "umbrella": False}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def surface(args: argparse.Namespace) -> dict:
|
|
532
|
+
records: list[dict] = []
|
|
533
|
+
sets: dict[str, list[str]] = {}
|
|
534
|
+
warnings: list[str] = []
|
|
535
|
+
out: dict = {"candidates": None, "guards": None, "state2": None, "canonical": None,
|
|
536
|
+
"extraction": None, "extractionGaps": [], "excluded": {"outsideScope": []}}
|
|
537
|
+
|
|
538
|
+
meta = _object(args.metadata, "--metadata") if args.metadata else None
|
|
539
|
+
provenance = _object(args.provenance, "--provenance") if args.provenance else None
|
|
540
|
+
brief = read_brief(args.brief) if args.brief else None
|
|
541
|
+
language = (brief or {}).get("language")
|
|
542
|
+
if not language and meta is not None and isinstance(meta.get("language"), str):
|
|
543
|
+
language = meta["language"]
|
|
544
|
+
|
|
545
|
+
extracted = None
|
|
546
|
+
if args.extraction:
|
|
547
|
+
extracted = from_extraction(_object(args.extraction, "--extraction"), language)
|
|
548
|
+
records += extracted["records"]
|
|
549
|
+
sets.update(extracted["sets"])
|
|
550
|
+
out["extraction"] = extracted["extraction"]
|
|
551
|
+
out["extractionGaps"] = extracted["gaps"]
|
|
552
|
+
out["excluded"]["outsideScope"] = extracted["outside"]
|
|
553
|
+
for path in args.quick or []:
|
|
554
|
+
data = _object(path, "--quick")
|
|
555
|
+
records += from_quick(data)
|
|
556
|
+
warnings += [f"{Path(path).name}: {w}" for w in data.get("warnings") or [] if isinstance(w, str)]
|
|
557
|
+
violations: list[str] = []
|
|
558
|
+
for path in args.per_file or []:
|
|
559
|
+
found, errors = from_per_file(path)
|
|
560
|
+
records += found
|
|
561
|
+
violations += errors
|
|
562
|
+
if violations:
|
|
563
|
+
raise SchemaError(violations)
|
|
564
|
+
records += [_record(name, None, None, None, None, "by-eye") for name in args.name or [] if name]
|
|
565
|
+
|
|
566
|
+
# A surface read from the source takes no name from the provenance map or
|
|
567
|
+
# metadata.json: they are only the baselines of the candidates and guards.
|
|
568
|
+
source_read = extracted is not None or bool(args.quick) or bool(args.per_file)
|
|
569
|
+
if not source_read and (provenance is not None or meta is not None):
|
|
570
|
+
named = [e for e in _entries(provenance)
|
|
571
|
+
if isinstance(e.get("export_name"), str) and e["export_name"]
|
|
572
|
+
and METHOD_SEPARATOR not in e["export_name"]]
|
|
573
|
+
prov_names = sorted({e["export_name"] for e in named})
|
|
574
|
+
exports = meta.get("exports") if meta is not None else None
|
|
575
|
+
meta_names = sorted({n for n in (_export_name(e) for e in exports) if n}) if isinstance(exports, list) else []
|
|
576
|
+
base_of: dict[str, str] = {}
|
|
577
|
+
if args.fold:
|
|
578
|
+
# One fold over both lists, so a variant folded out of the map
|
|
579
|
+
# never comes back through metadata.json.
|
|
580
|
+
base_of, tally = fold_names(sorted(set(prov_names) | set(meta_names)), args.fold_prefix or [],
|
|
581
|
+
set(args.keep or []), tallied=set(prov_names))
|
|
582
|
+
if provenance is not None:
|
|
583
|
+
canonical = sorted({base_of.get(n, n) for n in prov_names})
|
|
584
|
+
out["canonical"] = {
|
|
585
|
+
"raw": len(prov_names),
|
|
586
|
+
"canonical": len(canonical),
|
|
587
|
+
"folded": tally,
|
|
588
|
+
"variants": {n: base_of[n] for n in prov_names if n in base_of},
|
|
589
|
+
"summary": (f"Provenance-map canonicalization: {len(prov_names)} raw entries → "
|
|
590
|
+
f"{len(canonical)} canonical bases ({len(prov_names) - len(canonical)} "
|
|
591
|
+
f"bookkeeping variants folded: _def×{tally['_def']}, _exact×{tally['_exact']}, "
|
|
592
|
+
f"a11y_×{tally[FOLD_PREFIX]}, other×{tally['other']})"),
|
|
593
|
+
}
|
|
594
|
+
seen = set()
|
|
595
|
+
for e in named:
|
|
596
|
+
name = base_of.get(e["export_name"], e["export_name"])
|
|
597
|
+
if name in seen:
|
|
598
|
+
continue
|
|
599
|
+
seen.add(name)
|
|
600
|
+
records.append(_record(name, e.get("export_type"), e.get("source_file"), e.get("source_line"),
|
|
601
|
+
None, "provenance"))
|
|
602
|
+
meta_bases = sorted({base_of.get(n, n) for n in meta_names})
|
|
603
|
+
records += [_record(n, None, None, None, None, "metadata") for n in meta_bases]
|
|
604
|
+
if seen and meta_bases:
|
|
605
|
+
out["state2"] = {
|
|
606
|
+
"provenanceCount": len(seen),
|
|
607
|
+
"metadataCount": len(meta_bases),
|
|
608
|
+
"unionCount": len(seen | set(meta_bases)),
|
|
609
|
+
"metadataOnly": sorted(set(meta_bases) - seen),
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
records.sort(key=lambda r: (r["name"], str(r["file"] or ""), r["origin"]))
|
|
613
|
+
sets["all"] = sorted({r["name"] for r in records})
|
|
614
|
+
|
|
615
|
+
# The scope: the one the extractor recorded, else the brief's.
|
|
616
|
+
scope = extracted["scope"] if extracted is not None and any(extracted["scope"].values()) else None
|
|
617
|
+
if scope is None:
|
|
618
|
+
scope = brief["scope"] if brief is not None else _scope(None, None, None)
|
|
619
|
+
filed = [r for r in records if r["file"]]
|
|
620
|
+
if filed and scope["include"]:
|
|
621
|
+
sets["scope.include"] = matching(filed, scope["include"], scope["exclude"])
|
|
622
|
+
if filed and scope["tier_a_include"]:
|
|
623
|
+
sets["tier_a_include"] = matching(filed, scope["tier_a_include"], scope["exclude"])
|
|
624
|
+
|
|
625
|
+
if extracted is not None or brief is not None or meta is not None:
|
|
626
|
+
stats = meta.get("stats") if isinstance(meta, dict) and isinstance(meta.get("stats"), dict) else {}
|
|
627
|
+
stats_eff = _count(stats.get("effective_denominator"))
|
|
628
|
+
include_union = len(sets["scope.include"]) if "scope.include" in sets else len(sets["all"])
|
|
629
|
+
extraction = extracted["extraction"] if extracted is not None else {}
|
|
630
|
+
out["candidates"] = {
|
|
631
|
+
"statsEffectiveDenominator": stats_eff,
|
|
632
|
+
"tierAIncludeUnion": len(sets["tier_a_include"]) if "tier_a_include" in sets else None,
|
|
633
|
+
"scopeIncludeUnion": len(sets["scope.include"]) if "scope.include" in sets else None,
|
|
634
|
+
"subpathUnion": len(sets["subpaths"]) if "subpaths" in sets else None,
|
|
635
|
+
"rootBarrel": len(sets["root"]) if "root" in sets else None,
|
|
636
|
+
"extractorEffectiveDenominator": extraction.get("effectiveDenominator"),
|
|
637
|
+
"extractorBasis": extraction.get("effectiveDenominatorBasis"),
|
|
638
|
+
}
|
|
639
|
+
no_tier_a = not scope["tier_a_include"]
|
|
640
|
+
deflation = {"applicable": source_read and bool(stats_eff), "effectiveDenominator": stats_eff,
|
|
641
|
+
"rederived": include_union if source_read else None, "pct": None, "fires": False}
|
|
642
|
+
if deflation["applicable"]:
|
|
643
|
+
deflation["pct"] = round2((include_union - stats_eff) / stats_eff * 100)
|
|
644
|
+
deflation["fires"] = no_tier_a and deflation["pct"] > DEFLATION_PCT
|
|
645
|
+
entry_count = len(_entries(provenance)) if provenance is not None else 0
|
|
646
|
+
inflation = {"applicable": source_read and no_tier_a and bool(scope["include"]) and entry_count > 0,
|
|
647
|
+
"scopeIncludeUnion": include_union if scope["include"] else None,
|
|
648
|
+
"provenanceEntries": entry_count if provenance is not None else None,
|
|
649
|
+
"pct": None, "fires": False}
|
|
650
|
+
if inflation["applicable"]:
|
|
651
|
+
inflation["pct"] = round2((include_union - entry_count) / entry_count * 100)
|
|
652
|
+
inflation["fires"] = inflation["pct"] > INFLATION_PCT
|
|
653
|
+
out["guards"] = {
|
|
654
|
+
"deflation": deflation,
|
|
655
|
+
"inflation": inflation,
|
|
656
|
+
"umbrella": extracted["umbrella"] if extracted is not None else dict(NO_UMBRELLA),
|
|
657
|
+
"thresholds": {"deflationPct": DEFLATION_PCT, "inflationPct": INFLATION_PCT,
|
|
658
|
+
"umbrellaRatio": UMBRELLA_RATIO},
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
"inputs": {
|
|
663
|
+
"extraction": args.extraction, "quick": args.quick or [], "perFile": args.per_file or [],
|
|
664
|
+
"names": args.name or [], "provenance": args.provenance, "metadata": args.metadata,
|
|
665
|
+
"brief": args.brief, "fold": bool(args.fold),
|
|
666
|
+
},
|
|
667
|
+
"exports": records,
|
|
668
|
+
"sets": sets,
|
|
669
|
+
**out,
|
|
670
|
+
"warnings": warnings,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
# --------------------------------------------------------------------------
|
|
675
|
+
# CLI
|
|
676
|
+
# --------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _cmd_census(args: argparse.Namespace) -> dict:
|
|
680
|
+
return census(args.skill_dir)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _cmd_metadata(args: argparse.Namespace) -> dict:
|
|
684
|
+
meta = _object(args.metadata, "--metadata")
|
|
685
|
+
provenance = _object(args.provenance, "--provenance") if args.provenance else None
|
|
686
|
+
return metadata_inputs(meta, provenance)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
690
|
+
parser = argparse.ArgumentParser(
|
|
691
|
+
prog="load-coverage-inputs",
|
|
692
|
+
description="The counts, name sets and denominator candidates coverage-check reads, from files.",
|
|
693
|
+
)
|
|
694
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
695
|
+
|
|
696
|
+
p = sub.add_parser("census", help="count the citation forms: is the skill docs-only?")
|
|
697
|
+
p.add_argument("--skill-dir", required=True, help="the skill package: SKILL.md and references/")
|
|
698
|
+
p.add_argument("--output", help="also write the result to this file")
|
|
699
|
+
p.set_defaults(func=_cmd_census)
|
|
700
|
+
|
|
701
|
+
p = sub.add_parser("metadata", help="the metadata.json and provenance-map counts")
|
|
702
|
+
p.add_argument("--metadata", required=True, help="the skill's metadata.json")
|
|
703
|
+
p.add_argument("--provenance", help="the provenance map, when there is one")
|
|
704
|
+
p.add_argument("--output", help="also write the result to this file")
|
|
705
|
+
p.set_defaults(func=_cmd_metadata)
|
|
706
|
+
|
|
707
|
+
p = sub.add_parser("surface", help="the enumerated source surface, its name sets and candidates")
|
|
708
|
+
p.add_argument("--extraction", help="skf-extract-public-api.py --mode full output")
|
|
709
|
+
p.add_argument("--quick", action="append", help="skf-extract-public-api.py --mode quick output (repeatable)")
|
|
710
|
+
p.add_argument("--per-file", action="append", help="a subagent per-file result (repeatable)")
|
|
711
|
+
p.add_argument("--name", action="append", help="a name read by eye whose file is not known (repeatable)")
|
|
712
|
+
p.add_argument("--provenance", help="the provenance map: its named exports")
|
|
713
|
+
p.add_argument("--metadata", help="metadata.json: its exports[] names, stats and language")
|
|
714
|
+
p.add_argument("--brief", help="the skill brief: its scope globs and language")
|
|
715
|
+
p.add_argument("--fold", action="store_true", help="fold the provenance and metadata bookkeeping variants")
|
|
716
|
+
p.add_argument("--fold-prefix", action="append", help="a renderer prefix to fold (repeatable)")
|
|
717
|
+
p.add_argument("--keep", action="append", help="a variant that is a real export (repeatable)")
|
|
718
|
+
p.add_argument("--output", help="also write the result to this file")
|
|
719
|
+
p.set_defaults(func=surface)
|
|
720
|
+
return parser
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def main(argv: list[str] | None = None) -> int:
|
|
724
|
+
args = _build_parser().parse_args(argv)
|
|
725
|
+
try:
|
|
726
|
+
result = args.func(args)
|
|
727
|
+
except InputError as exc:
|
|
728
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
729
|
+
return 1
|
|
730
|
+
except SchemaError as exc:
|
|
731
|
+
if args.output:
|
|
732
|
+
Path(args.output).unlink(missing_ok=True)
|
|
733
|
+
print(json.dumps({"valid": False, "violations": exc.violations}, indent=2))
|
|
734
|
+
return 2
|
|
735
|
+
if args.output:
|
|
736
|
+
target = Path(args.output)
|
|
737
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
738
|
+
target.write_bytes((json.dumps(result, indent=2, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
739
|
+
print(json.dumps(result, indent=2))
|
|
740
|
+
return 0
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _force_utf8(*streams) -> None:
|
|
744
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
745
|
+
|
|
746
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
747
|
+
of the --help text, so --help would stop with UnicodeEncodeError.
|
|
748
|
+
"""
|
|
749
|
+
for stream in streams:
|
|
750
|
+
if hasattr(stream, "reconfigure"):
|
|
751
|
+
errors = getattr(stream, "errors", None)
|
|
752
|
+
if errors is None:
|
|
753
|
+
stream.reconfigure(encoding="utf-8")
|
|
754
|
+
else:
|
|
755
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
if __name__ == "__main__":
|
|
759
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
760
|
+
sys.exit(main())
|