bmad-module-skill-forge 2.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +143 -18
- package/docs/verifying-a-skill.md +87 -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 +3 -3
- 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 +31 -59
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- 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 +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- 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 +21 -3
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
- 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 +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- 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 +155 -14
- 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 +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- 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 +23 -2
- 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 +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- 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 +28 -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/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- 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 +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- 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 +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- 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 +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- 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 +119 -70
- 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 +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- 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 +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- 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 +122 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +209 -264
- 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 +164 -91
- 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 +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- 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 +380 -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 +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -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 +133 -56
- 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 +253 -223
- 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 +254 -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 +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
3
|
-
# dependencies = []
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Merge Doc URLs — merge corpora-seeded + detected docs for a skill brief.
|
|
6
6
|
|
|
7
7
|
Deterministic merge of an upstream brief's `doc_urls` (registry-seeded
|
|
8
8
|
companion corpora, source: language-registry) with the docs freshly
|
|
9
9
|
discovered by `skf-detect-docs.py` (mapped to the brief `{url, label, source}`
|
|
10
|
-
contract). Replaces the prose dedup
|
|
11
|
-
`skf-brief-skill/references/step-auto-brief.md` §3
|
|
10
|
+
contract). Replaces the prose mapping, dedup and suppression that lived inline
|
|
11
|
+
in `skf-brief-skill/references/step-auto-brief.md` §3. That logic is fiddly
|
|
12
12
|
(normalized-URL collisions, non-corpus path segments, locale duplicates) and
|
|
13
13
|
must behave identically on every run, so it belongs in a tested script.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
With --detected-file, each skf-detect-docs.py entry {url, detected_via,
|
|
16
|
+
content_type} is first mapped to {url, label, source}: `source` from
|
|
17
|
+
`detected_via` and `label` from `content_type`, by the two tables below (a
|
|
18
|
+
content_type they do not list gives the label ""). An entry whose
|
|
19
|
+
`detected_via` they do not list is never merged: `suppressed` lists it with
|
|
20
|
+
reason unknown-detected-via.
|
|
21
|
+
|
|
22
|
+
Three merge operations, in order:
|
|
16
23
|
|
|
17
24
|
1. Normalized-URL dedup — lowercase host, strip a trailing `/index.html`
|
|
18
25
|
and any trailing `/` before comparing, so a seeded `…/book/` and a
|
|
@@ -41,10 +48,18 @@ DEGRADED case is intentionally out of scope (there is no canonical corpus host
|
|
|
41
48
|
to anchor suppression against).
|
|
42
49
|
|
|
43
50
|
CLI:
|
|
51
|
+
uv run src/shared/scripts/skf-merge-doc-urls.py --scope-type <type> \
|
|
52
|
+
--detected-file <detect-docs.json> --existing-brief <skill-brief.yaml>
|
|
44
53
|
echo '{"scope_type": "...", "existing": [...], "detected": [...]}' \
|
|
45
54
|
| uv run src/shared/scripts/skf-merge-doc-urls.py
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
Files form (stdin is not read): --detected-file holds skf-detect-docs.py's
|
|
57
|
+
JSON array, mapped as above; --existing-brief is a skill-brief.yaml whose
|
|
58
|
+
`doc_urls` are the existing entries (none when it has no doc_urls), read as
|
|
59
|
+
skf-resolve-authoritative-files.py reads a brief; --scope-type is the scope
|
|
60
|
+
type. Each flag is optional: an absent one gives no entries (or "").
|
|
61
|
+
|
|
62
|
+
Input of the stdin form (a JSON object), whose entries are merged as given:
|
|
48
63
|
{
|
|
49
64
|
"scope_type": "full-library",
|
|
50
65
|
"existing": [{"url": "...", "label": "...", "source": "language-registry"}, ...],
|
|
@@ -54,22 +69,42 @@ Input (JSON object on stdin):
|
|
|
54
69
|
Output (JSON object on stdout):
|
|
55
70
|
{
|
|
56
71
|
"doc_urls": [{"url": "...", "label": "...", "source": "..."}, ...],
|
|
57
|
-
"suppressed": [{"url": "...", "reason":
|
|
72
|
+
"suppressed": [{"url": "...", "reason":
|
|
73
|
+
"unknown-detected-via|non-corpus-segment|non-primary-locale-dup"}, ...]
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
Exit codes:
|
|
61
77
|
0 success (merged list emitted)
|
|
62
|
-
2 error (bad JSON, wrong input shape
|
|
78
|
+
2 error (bad JSON, wrong input shape, a file that cannot be read, a brief
|
|
79
|
+
that is not a YAML mapping)
|
|
63
80
|
"""
|
|
64
81
|
|
|
65
82
|
from __future__ import annotations
|
|
66
83
|
|
|
84
|
+
import argparse
|
|
85
|
+
import importlib.util
|
|
67
86
|
import json
|
|
68
87
|
import re
|
|
69
88
|
import sys
|
|
89
|
+
from pathlib import Path
|
|
70
90
|
from typing import Any
|
|
71
91
|
from urllib.parse import urlsplit
|
|
72
92
|
|
|
93
|
+
# skf-detect-docs.py's `detected_via` -> the brief's coarse provenance, the
|
|
94
|
+
# skill-brief.v1.json doc_urls[].source enum (a test pins it to the schema).
|
|
95
|
+
_SOURCE_BY_DETECTED_VIA = {
|
|
96
|
+
"homepageUrl": "homepage",
|
|
97
|
+
"readme_link": "readme-detection",
|
|
98
|
+
"pages_api": "pages-api",
|
|
99
|
+
"docs_folder": "docs-folder",
|
|
100
|
+
}
|
|
101
|
+
# skf-detect-docs.py's `content_type` -> the entry's display label.
|
|
102
|
+
_LABEL_BY_CONTENT_TYPE = {
|
|
103
|
+
"api-docs": "API Documentation",
|
|
104
|
+
"guide": "Guide",
|
|
105
|
+
"reference": "Reference",
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
# Path components (case-insensitive) that mark a non-corpus page on a doc host:
|
|
74
109
|
# changelogs/news, contribution guides, wikis. Matched as whole path components
|
|
75
110
|
# (or component + a separator), never as a bare substring.
|
|
@@ -153,6 +188,28 @@ def _matches_non_corpus_segment(path: str) -> bool:
|
|
|
153
188
|
return False
|
|
154
189
|
|
|
155
190
|
|
|
191
|
+
def map_detected(raw: list[Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
192
|
+
"""skf-detect-docs.py entries as brief doc_urls entries {url, label, source}.
|
|
193
|
+
|
|
194
|
+
Returns (mapped, suppressed): an entry whose detected_via maps to no
|
|
195
|
+
source is suppressed with reason unknown-detected-via; one with no url is
|
|
196
|
+
skipped, as the merge skips it.
|
|
197
|
+
"""
|
|
198
|
+
mapped: list[dict[str, Any]] = []
|
|
199
|
+
suppressed: list[dict[str, Any]] = []
|
|
200
|
+
for d in raw:
|
|
201
|
+
if not isinstance(d, dict) or not d.get("url"):
|
|
202
|
+
continue
|
|
203
|
+
via, content_type = d.get("detected_via"), d.get("content_type")
|
|
204
|
+
source = _SOURCE_BY_DETECTED_VIA.get(via) if isinstance(via, str) else None
|
|
205
|
+
if source is None:
|
|
206
|
+
suppressed.append({"url": d["url"], "reason": "unknown-detected-via"})
|
|
207
|
+
continue
|
|
208
|
+
label = _LABEL_BY_CONTENT_TYPE.get(content_type, "") if isinstance(content_type, str) else ""
|
|
209
|
+
mapped.append({"url": d["url"], "label": label, "source": source})
|
|
210
|
+
return mapped, suppressed
|
|
211
|
+
|
|
212
|
+
|
|
156
213
|
def merge_doc_urls(
|
|
157
214
|
scope_type: str,
|
|
158
215
|
existing: list[dict[str, Any]],
|
|
@@ -232,30 +289,96 @@ def _force_utf8(*streams) -> None:
|
|
|
232
289
|
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
233
290
|
|
|
234
291
|
|
|
235
|
-
|
|
236
|
-
|
|
292
|
+
class InputError(Exception):
|
|
293
|
+
"""An input the merge cannot read (exit 2)."""
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _read_detected(path: str) -> list[Any]:
|
|
297
|
+
"""The JSON array skf-detect-docs.py wrote to `path`."""
|
|
298
|
+
try:
|
|
299
|
+
data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
300
|
+
except OSError as exc:
|
|
301
|
+
raise InputError(f"cannot read --detected-file {path}: {exc.strerror or exc}") from exc
|
|
302
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
303
|
+
raise InputError(f"--detected-file {path} is not JSON: {exc}") from exc
|
|
304
|
+
if not isinstance(data, list):
|
|
305
|
+
raise InputError(f"--detected-file {path} must hold a JSON array (skf-detect-docs.py output)")
|
|
306
|
+
return data
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _brief_doc_urls(path: str) -> list[Any]:
|
|
310
|
+
"""The doc_urls of the skill brief at `path`, read by
|
|
311
|
+
skf-resolve-authoritative-files.py's parse_brief_yaml (beside this file)."""
|
|
312
|
+
resolver_path = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
|
|
313
|
+
spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", resolver_path)
|
|
314
|
+
if spec is None or spec.loader is None:
|
|
315
|
+
raise InputError(f"cannot load {resolver_path.name} beside {Path(__file__).name}")
|
|
316
|
+
resolver = importlib.util.module_from_spec(spec)
|
|
317
|
+
try:
|
|
318
|
+
spec.loader.exec_module(resolver)
|
|
319
|
+
except (OSError, ImportError, SyntaxError) as exc:
|
|
320
|
+
raise InputError(f"cannot load {resolver_path.name} beside {Path(__file__).name}: {exc}") from exc
|
|
321
|
+
try:
|
|
322
|
+
brief = resolver.parse_brief_yaml(Path(path).read_bytes().decode("utf-8-sig"))
|
|
323
|
+
except OSError as exc:
|
|
324
|
+
raise InputError(f"cannot read --existing-brief {path}: {exc.strerror or exc}") from exc
|
|
325
|
+
except (UnicodeDecodeError, resolver.yaml.YAMLError) as exc:
|
|
326
|
+
raise InputError(f"--existing-brief {path} is not YAML: {exc}") from exc
|
|
327
|
+
if not isinstance(brief, dict):
|
|
328
|
+
raise InputError(f"--existing-brief {path} holds no brief mapping")
|
|
329
|
+
doc_urls = brief.get("doc_urls") or []
|
|
330
|
+
if not isinstance(doc_urls, list):
|
|
331
|
+
raise InputError(f"the doc_urls of --existing-brief {path} must be a list")
|
|
332
|
+
return doc_urls
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _stdin_payload() -> tuple[str, list[Any], list[Any]]:
|
|
336
|
+
"""(scope_type, existing, detected) of the stdin form."""
|
|
237
337
|
raw = sys.stdin.read()
|
|
238
338
|
if not raw or not raw.strip():
|
|
239
|
-
|
|
240
|
-
return 2
|
|
339
|
+
raise InputError("empty stdin (expected JSON object)")
|
|
241
340
|
try:
|
|
242
341
|
payload = json.loads(raw)
|
|
243
342
|
except json.JSONDecodeError as exc:
|
|
244
|
-
|
|
245
|
-
return 2
|
|
343
|
+
raise InputError(f"invalid JSON on stdin: {exc}") from exc
|
|
246
344
|
if not isinstance(payload, dict):
|
|
247
|
-
|
|
248
|
-
return 2
|
|
249
|
-
|
|
250
|
-
scope_type = payload.get("scope_type") or ""
|
|
345
|
+
raise InputError("input must be a JSON object")
|
|
251
346
|
existing = payload.get("existing") or []
|
|
252
347
|
detected = payload.get("detected") or []
|
|
253
348
|
if not isinstance(existing, list) or not isinstance(detected, list):
|
|
254
|
-
|
|
349
|
+
raise InputError("'existing' and 'detected' must be arrays")
|
|
350
|
+
return payload.get("scope_type") or "", existing, detected
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def main(argv: list[str] | None = None) -> int:
|
|
354
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
355
|
+
parser = argparse.ArgumentParser(
|
|
356
|
+
prog="skf-merge-doc-urls",
|
|
357
|
+
description="Merge a brief's doc_urls with the docs skf-detect-docs.py found. With no flag, read the "
|
|
358
|
+
"{scope_type, existing, detected} JSON object on stdin.",
|
|
359
|
+
)
|
|
360
|
+
parser.add_argument("--detected-file", metavar="PATH",
|
|
361
|
+
help="skf-detect-docs.py's JSON array, mapped to {url, label, source}")
|
|
362
|
+
parser.add_argument("--existing-brief", metavar="PATH",
|
|
363
|
+
help="the skill-brief.yaml whose doc_urls are the existing entries")
|
|
364
|
+
parser.add_argument("--scope-type", metavar="TYPE", help="the brief's scope.type")
|
|
365
|
+
args = parser.parse_args(argv)
|
|
366
|
+
unknown: list[dict[str, Any]] = []
|
|
367
|
+
try:
|
|
368
|
+
if args.detected_file is None and args.existing_brief is None and args.scope_type is None:
|
|
369
|
+
scope_type, existing, detected = _stdin_payload()
|
|
370
|
+
else:
|
|
371
|
+
scope_type = args.scope_type or ""
|
|
372
|
+
existing = _brief_doc_urls(args.existing_brief) if args.existing_brief is not None else []
|
|
373
|
+
detected = []
|
|
374
|
+
if args.detected_file is not None:
|
|
375
|
+
detected, unknown = map_detected(_read_detected(args.detected_file))
|
|
376
|
+
except InputError as exc:
|
|
377
|
+
sys.stderr.write(f"skf-merge-doc-urls: {exc}\n")
|
|
255
378
|
return 2
|
|
256
379
|
|
|
257
380
|
doc_urls, suppressed = merge_doc_urls(scope_type, existing, detected)
|
|
258
|
-
json.dump({"doc_urls": doc_urls, "suppressed": suppressed}, sys.stdout, ensure_ascii=False)
|
|
381
|
+
json.dump({"doc_urls": doc_urls, "suppressed": unknown + suppressed}, sys.stdout, ensure_ascii=False)
|
|
259
382
|
sys.stdout.write("\n")
|
|
260
383
|
return 0
|
|
261
384
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Names Present: the provenance entries whose export name a skill package never writes.
|
|
6
|
+
|
|
7
|
+
skf-test-skill credits a provenance entry toward a stack's Export Coverage
|
|
8
|
+
only when the package writes its `export_name`: reconcile-coverage.py
|
|
9
|
+
(`load_doc_text`, `names_present`) joins the text of `SKILL.md` and of each
|
|
10
|
+
`.md` file directly in `references/` (sorted, read as UTF-8, no subfolder)
|
|
11
|
+
and looks for the name in it as a fixed string, case-sensitive, never inside
|
|
12
|
+
a longer identifier: on a side where the name ends in a letter, digit, `_`
|
|
13
|
+
or `$`, the next character may not be one of those, so `get` is not found in
|
|
14
|
+
`target` or `getAll` (skf-test-skill's validate-inventory.py keeps that
|
|
15
|
+
rule; NAME_CHARS here must stay the same). A name holding
|
|
16
|
+
`::` (an impl-block method, `Type::method`) rolls up under its type and is
|
|
17
|
+
never looked up, and an entry with no name is no cited contract.
|
|
18
|
+
create-stack-skill (generate-output section 8) runs this helper on a
|
|
19
|
+
compose-mode map before the commit, so a stack never commits a name
|
|
20
|
+
skf-test-skill will not credit. The rule here is the scorer's:
|
|
21
|
+
test/test-skf-stack-step-rules.py runs both on the same package.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
uv run skf-names-present.py --provenance <provenance-map.json> \\
|
|
25
|
+
--skill-dir <dir> [--drop-absent]
|
|
26
|
+
|
|
27
|
+
Output (stdout):
|
|
28
|
+
{
|
|
29
|
+
"checked": <int>, # entries whose export_name was looked up
|
|
30
|
+
"skipped": <int>, # entries with a `::` name or no name
|
|
31
|
+
"absent": [ # in entries[] order
|
|
32
|
+
{"entry_index": <int>, "export_name": "<name>",
|
|
33
|
+
"source_library": "<library>" | null},
|
|
34
|
+
...
|
|
35
|
+
],
|
|
36
|
+
"dropped": [...], # --drop-absent: the absent entries removed
|
|
37
|
+
"provenance_written": <bool>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
--drop-absent removes every absent entry from the map and rewrites it
|
|
41
|
+
through skf-atomic-write.py beside this script (JSON indented by two
|
|
42
|
+
spaces, non-ASCII kept, a final newline). `absent[]` still lists what the
|
|
43
|
+
lookup found, and `dropped[]` repeats it. `entry_index` is the position in
|
|
44
|
+
the map as it was read.
|
|
45
|
+
|
|
46
|
+
Exit codes:
|
|
47
|
+
0 no entry is absent, or --drop-absent removed every absent entry
|
|
48
|
+
1 an entry is absent (absent[] lists it)
|
|
49
|
+
2 usage, file, JSON or write error (one line on stderr, no JSON)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import re
|
|
57
|
+
import subprocess
|
|
58
|
+
import sys
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
ATOMIC_WRITE_HELPER = "skf-atomic-write.py"
|
|
62
|
+
ATOMIC_WRITE_TIMEOUT_SEC = 60
|
|
63
|
+
METHOD_SEPARATOR = "::"
|
|
64
|
+
# Characters that continue an identifier in the languages SKF documents (`$`
|
|
65
|
+
# for JS/TS names such as `$state`): skf-test-skill's IDENT_CHARS.
|
|
66
|
+
NAME_CHARS = r"\w$"
|
|
67
|
+
_NAME_CHAR_RE = re.compile(f"[{NAME_CHARS}]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_doc_text(skill_dir: Path) -> str:
|
|
71
|
+
"""The text skf-test-skill looks names up in: SKILL.md, then each .md file
|
|
72
|
+
directly in references/ in sorted order, joined by newlines. Raises
|
|
73
|
+
OSError, or UnicodeDecodeError for a file that is not UTF-8."""
|
|
74
|
+
parts = []
|
|
75
|
+
skill_md = skill_dir / "SKILL.md"
|
|
76
|
+
if skill_md.is_file():
|
|
77
|
+
parts.append(skill_md.read_text(encoding="utf-8"))
|
|
78
|
+
refs = skill_dir / "references"
|
|
79
|
+
if refs.is_dir():
|
|
80
|
+
for ref in sorted(refs.glob("*.md")):
|
|
81
|
+
parts.append(ref.read_text(encoding="utf-8"))
|
|
82
|
+
return "\n".join(parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def name_written(name: str, doc_text: str) -> bool:
|
|
86
|
+
"""True when `doc_text` holds `name` as skf-test-skill's scorer finds it."""
|
|
87
|
+
body = re.escape(name)
|
|
88
|
+
if _NAME_CHAR_RE.match(name[0]):
|
|
89
|
+
body = f"(?<![{NAME_CHARS}])" + body
|
|
90
|
+
if _NAME_CHAR_RE.match(name[-1]):
|
|
91
|
+
body += f"(?![{NAME_CHARS}])"
|
|
92
|
+
return re.search(body, doc_text) is not None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def looked_up_name(entry: object) -> str | None:
|
|
96
|
+
"""The name skf-test-skill looks up for an entry, or None when it looks
|
|
97
|
+
up none: no string name, or a `Type::method` name."""
|
|
98
|
+
if not isinstance(entry, dict):
|
|
99
|
+
return None
|
|
100
|
+
name = entry.get("export_name")
|
|
101
|
+
if not isinstance(name, str) or not name or METHOD_SEPARATOR in name:
|
|
102
|
+
return None
|
|
103
|
+
return name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_absent(entries: list, doc_text: str) -> tuple[int, list[dict]]:
|
|
107
|
+
"""(entries looked up, the absent ones in entries[] order)."""
|
|
108
|
+
checked, absent = 0, []
|
|
109
|
+
for index, entry in enumerate(entries):
|
|
110
|
+
name = looked_up_name(entry)
|
|
111
|
+
if name is None:
|
|
112
|
+
continue
|
|
113
|
+
checked += 1
|
|
114
|
+
if not name_written(name, doc_text):
|
|
115
|
+
library = entry.get("source_library")
|
|
116
|
+
absent.append({
|
|
117
|
+
"entry_index": index,
|
|
118
|
+
"export_name": name,
|
|
119
|
+
"source_library": library if isinstance(library, str) else None,
|
|
120
|
+
})
|
|
121
|
+
return checked, absent
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def atomic_write(target: Path, data: bytes) -> None:
|
|
125
|
+
"""Write `data` to `target` through skf-atomic-write.py beside this
|
|
126
|
+
script. Raises OSError when the helper is missing or fails."""
|
|
127
|
+
helper = Path(__file__).resolve().parent / ATOMIC_WRITE_HELPER
|
|
128
|
+
if not helper.is_file():
|
|
129
|
+
raise OSError(f"{ATOMIC_WRITE_HELPER} not found beside {Path(__file__).name}; nothing written")
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
[sys.executable, str(helper), "write", "--target", str(target)],
|
|
133
|
+
input=data,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
timeout=ATOMIC_WRITE_TIMEOUT_SEC,
|
|
136
|
+
check=False,
|
|
137
|
+
)
|
|
138
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
139
|
+
raise OSError(f"atomic write of {target} failed: {exc}") from exc
|
|
140
|
+
if result.returncode != 0:
|
|
141
|
+
detail = (result.stderr or b"").decode("utf-8", errors="replace").strip()
|
|
142
|
+
raise OSError(f"atomic write of {target} failed: {detail}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _fail(message: str) -> int:
|
|
146
|
+
print(f"error: {' '.join(message.split())}", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
151
|
+
parser = argparse.ArgumentParser(
|
|
152
|
+
prog="skf-names-present",
|
|
153
|
+
description=(
|
|
154
|
+
"List the provenance entries whose export_name the skill package "
|
|
155
|
+
"never writes, by skf-test-skill's Export Coverage rule "
|
|
156
|
+
"(SKILL.md and references/*.md, case-sensitive substring, `::` "
|
|
157
|
+
"names skipped); with --drop-absent, remove them from the map."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument("--provenance", required=True, help="path to provenance-map.json")
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--skill-dir",
|
|
163
|
+
required=True,
|
|
164
|
+
help="skill package folder holding the SKILL.md and references/ to search",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--drop-absent",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="remove every absent entry and rewrite the map through skf-atomic-write.py",
|
|
170
|
+
)
|
|
171
|
+
return parser
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main(argv: list[str] | None = None) -> int:
|
|
175
|
+
args = _build_parser().parse_args(argv)
|
|
176
|
+
prov_path = Path(args.provenance)
|
|
177
|
+
skill_dir = Path(args.skill_dir)
|
|
178
|
+
if not prov_path.is_file():
|
|
179
|
+
return _fail(f"provenance map not found: {prov_path}")
|
|
180
|
+
# A wrong folder would read every name as absent, and --drop-absent
|
|
181
|
+
# would then empty the map: require the package's SKILL.md.
|
|
182
|
+
if not (skill_dir / "SKILL.md").is_file():
|
|
183
|
+
return _fail(f"skill dir has no SKILL.md: {skill_dir}")
|
|
184
|
+
try:
|
|
185
|
+
prov = json.loads(prov_path.read_text(encoding="utf-8"))
|
|
186
|
+
except (OSError, ValueError) as exc:
|
|
187
|
+
return _fail(f"cannot read the provenance map {prov_path}: {exc}")
|
|
188
|
+
entries = prov.get("entries") if isinstance(prov, dict) else None
|
|
189
|
+
if not isinstance(entries, list):
|
|
190
|
+
return _fail(f"the provenance map {prov_path} has no entries[] list")
|
|
191
|
+
try:
|
|
192
|
+
doc_text = load_doc_text(skill_dir)
|
|
193
|
+
except (OSError, ValueError) as exc:
|
|
194
|
+
return _fail(f"cannot read the skill package {skill_dir}: {exc}")
|
|
195
|
+
|
|
196
|
+
checked, absent = find_absent(entries, doc_text)
|
|
197
|
+
dropped: list[dict] = []
|
|
198
|
+
if args.drop_absent and absent:
|
|
199
|
+
gone = {item["entry_index"] for item in absent}
|
|
200
|
+
prov["entries"] = [entry for index, entry in enumerate(entries) if index not in gone]
|
|
201
|
+
data = (json.dumps(prov, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
|
|
202
|
+
try:
|
|
203
|
+
atomic_write(prov_path, data)
|
|
204
|
+
except OSError as exc:
|
|
205
|
+
return _fail(str(exc))
|
|
206
|
+
dropped = absent
|
|
207
|
+
|
|
208
|
+
result = {
|
|
209
|
+
"checked": checked,
|
|
210
|
+
"skipped": len(entries) - checked,
|
|
211
|
+
"absent": absent,
|
|
212
|
+
"dropped": dropped,
|
|
213
|
+
"provenance_written": bool(dropped),
|
|
214
|
+
}
|
|
215
|
+
# ASCII escapes: a Windows console's code page cannot encode every name.
|
|
216
|
+
sys.stdout.write(json.dumps(result, indent=2) + "\n")
|
|
217
|
+
return 0 if not absent or dropped else 1
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|