bmad-module-skill-forge 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,104 +1,125 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
|
-
"""SKF Recommend Scope Type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
The
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
5
|
+
"""SKF Recommend Scope Type: the scope-type ladder, applied to facts and to
|
|
6
|
+
the intent signals the caller classified.
|
|
7
|
+
|
|
8
|
+
Single source of truth for the scope-type recommendation that
|
|
9
|
+
skf-brief-skill step 3 §2c presents and a headless run applies. Both
|
|
10
|
+
paths call this script with the same inputs, so they cannot drift.
|
|
11
|
+
|
|
12
|
+
The script reads no free text. What a user's intent asks for is a
|
|
13
|
+
judgment: "the whole library, not just the parser" names no module, and
|
|
14
|
+
"Kickstarter-style" asks for no starter app although it contains the word.
|
|
15
|
+
So the caller reads the intent and passes three typed signals, and the
|
|
16
|
+
script keeps only the rules that rest on facts (the file tree, a registry
|
|
17
|
+
file's contents, the module and export counts, the source type) and the
|
|
18
|
+
order of the ladder.
|
|
19
|
+
|
|
20
|
+
The ladder (first match wins):
|
|
21
|
+
|
|
22
|
+
0. docs-only source_type is docs-only: no source surface to scope
|
|
23
|
+
1. component-library a registry.ts / components.ts (or .tsx) at any depth
|
|
24
|
+
of the tree whose contents (entry_files, or
|
|
25
|
+
--entry-dir) show 10+ entries or a Component[]
|
|
26
|
+
annotation. In headless mode, a registry file whose
|
|
27
|
+
contents were not given counts by its presence.
|
|
28
|
+
2. reference-app signal wants_wiring_pattern
|
|
29
|
+
3. specific-modules signal named_module_subset is not empty, else
|
|
30
|
+
module_count >= 6
|
|
31
|
+
4. public-api signal wants_narrow_api and 0 < export_count <= 8
|
|
32
|
+
5. full-library no rule matched
|
|
32
33
|
|
|
33
34
|
CLI:
|
|
34
|
-
|
|
35
|
-
uv run skf-recommend-scope-type.py --json '{...}'
|
|
35
|
+
uv run skf-recommend-scope-type.py --tree-file <file> [--entry-dir <dir>] [--extract-file <file>] < payload.json
|
|
36
|
+
uv run skf-recommend-scope-type.py --tree-file <file> --json '{...}'
|
|
37
|
+
<listing> | uv run skf-recommend-scope-type.py --tree-file - --json '{...}'
|
|
38
|
+
uv run skf-recommend-scope-type.py --tree-file <file> --registry-files
|
|
39
|
+
|
|
40
|
+
--tree-file reads the repository's file list from a file (`-` reads it from
|
|
41
|
+
stdin, and the payload then comes with --json):
|
|
42
|
+
- JSON: an object whose `tree` lists the paths, such as the output of
|
|
43
|
+
skf-github-probe.py tree (a listing whose `status` is not "ok" is
|
|
44
|
+
refused, with its `message`) or a GitHub git/trees response (only its
|
|
45
|
+
`blob` entries count); or a JSON list of paths.
|
|
46
|
+
- Anything else: one path per line, as `git -c core.quotePath=false
|
|
47
|
+
ls-files` prints them (a leading `./`, as `find .` prints it, is
|
|
48
|
+
dropped).
|
|
49
|
+
The payload may carry the list as `tree` instead; passing both is an error.
|
|
50
|
+
|
|
51
|
+
--registry-files prints the registry files the --tree-file listing holds,
|
|
52
|
+
one repo path per line with LF endings on every platform (nothing when there
|
|
53
|
+
are none), and exits without reading a payload: a shell loop fetches exactly
|
|
54
|
+
those files.
|
|
55
|
+
|
|
56
|
+
--entry-dir reads each registry file the tree lists from <dir>/<repo path>,
|
|
57
|
+
as the payload's `entry_files`: a local checkout, or the folder the caller
|
|
58
|
+
fetched the --registry-files paths into. A registry file missing there is
|
|
59
|
+
one whose contents were not given. Passing `entry_files` too is an error.
|
|
60
|
+
|
|
61
|
+
--extract-file reads the extractor's output (skf-extract-public-api.py
|
|
62
|
+
--mode quick, or any JSON object with an `exports` list) and sets
|
|
63
|
+
`export_count` to the number of distinct export names it lists, so no
|
|
64
|
+
caller counts the exports by hand. Passing `export_count` too is an error.
|
|
36
65
|
|
|
37
66
|
Input (JSON object on stdin or via --json):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
signals object, required unless source_type is docs-only. Exactly
|
|
68
|
+
these keys, classified by the caller from the user's intent
|
|
69
|
+
and scope hints:
|
|
70
|
+
wants_wiring_pattern true | false: the user wants how an
|
|
71
|
+
app, starter or example wires its
|
|
72
|
+
parts together, not a library's API
|
|
73
|
+
named_module_subset the module names the user limits the
|
|
74
|
+
skill to ([] when none)
|
|
75
|
+
wants_narrow_api true | false: the user wants only the
|
|
76
|
+
public API, SDK or client surface
|
|
77
|
+
module_count integer >= 0 (top-level modules from step 2), default 0
|
|
78
|
+
export_count integer >= 0 (named exports from step 2), default 0 (or
|
|
79
|
+
--extract-file)
|
|
80
|
+
tree list of repo-relative file paths, default [] (or --tree-file)
|
|
81
|
+
entry_files optional [{path, content}]: the registry files' contents
|
|
82
|
+
(or --entry-dir)
|
|
83
|
+
source_type "source" | "docs-only" | null, default null
|
|
84
|
+
mode "interactive" | "headless", default "headless"
|
|
85
|
+
A payload that carries free text (`intent`, `scope_hint`) or any other key
|
|
86
|
+
is refused.
|
|
45
87
|
|
|
46
88
|
Output (JSON on stdout):
|
|
47
|
-
scope_type
|
|
48
|
-
|
|
49
|
-
matched_heuristic
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
signals
|
|
54
|
-
rationale
|
|
89
|
+
scope_type full-library | specific-modules | public-api
|
|
90
|
+
| component-library | reference-app | docs-only
|
|
91
|
+
matched_heuristic "docs-only-shortcircuit" | "component-registry"
|
|
92
|
+
| "reference-app-intent" | "specific-modules-naming"
|
|
93
|
+
| "specific-modules-count" | "narrow-public-api"
|
|
94
|
+
| "default-full-library"
|
|
95
|
+
signals what fired (a path, a count, the signal values)
|
|
96
|
+
rationale one sentence naming what fired
|
|
55
97
|
|
|
56
98
|
Exit codes:
|
|
57
|
-
0
|
|
58
|
-
2
|
|
99
|
+
0 recommendation produced (or, with --registry-files, the list printed)
|
|
100
|
+
2 bad input: invalid JSON, an unknown or mistyped key, no signals object,
|
|
101
|
+
the tree, the registry contents or the export count passed twice, a
|
|
102
|
+
tree listing that cannot be read, is empty or reports a failure, or an
|
|
103
|
+
--extract-file that cannot be read or holds no `exports` list
|
|
59
104
|
"""
|
|
60
105
|
|
|
61
106
|
from __future__ import annotations
|
|
62
107
|
|
|
63
108
|
import argparse
|
|
64
109
|
import json
|
|
110
|
+
import os
|
|
65
111
|
import re
|
|
66
112
|
import sys
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"example app",
|
|
78
|
-
"reference app",
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
NARROW_PUBLIC_API_KEYWORDS = (
|
|
82
|
-
"the api",
|
|
83
|
-
"the sdk",
|
|
84
|
-
"public api",
|
|
85
|
-
"client library",
|
|
86
|
-
"client sdk",
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
# Phrases that indicate the user wants only a specific subset of modules
|
|
90
|
-
# rather than the whole library. The match is substring + lowercase, so
|
|
91
|
-
# "just the auth module" / "only the streaming part" / "specifically the
|
|
92
|
-
# parser" all match.
|
|
93
|
-
SPECIFIC_MODULE_NAMING_PATTERNS = (
|
|
94
|
-
re.compile(r"\bjust the\s+\w+", re.IGNORECASE),
|
|
95
|
-
re.compile(r"\bonly the\s+\w+", re.IGNORECASE),
|
|
96
|
-
re.compile(r"\bspecifically the\s+\w+", re.IGNORECASE),
|
|
97
|
-
re.compile(r"\bjust\s+(?:want|need|use)\s+the\s+\w+", re.IGNORECASE),
|
|
98
|
-
re.compile(r"\bonly\s+(?:want|need|use)\s+the\s+\w+", re.IGNORECASE),
|
|
99
|
-
re.compile(r"\bjust\s+\w+\s+module\b", re.IGNORECASE),
|
|
100
|
-
re.compile(r"\bonly\s+\w+\s+module\b", re.IGNORECASE),
|
|
101
|
-
)
|
|
113
|
+
|
|
114
|
+
SIGNAL_KEYS = ("wants_wiring_pattern", "named_module_subset", "wants_narrow_api")
|
|
115
|
+
PAYLOAD_KEYS = frozenset({"signals", "module_count", "export_count", "tree", "entry_files", "source_type", "mode"})
|
|
116
|
+
# Free-text keys (the old payload carried `intent`), refused by name: the
|
|
117
|
+
# script reads no free text.
|
|
118
|
+
FREE_TEXT_KEYS = ("intent", "scope_hint")
|
|
119
|
+
# A --tree-file listing is JSON when it opens like a JSON object or list of
|
|
120
|
+
# paths. A bare leading bracket is not enough: a line listing can start with
|
|
121
|
+
# a path such as `[slug]/page.tsx`.
|
|
122
|
+
JSON_LISTING_RE = re.compile(r'\{\s*["}]|\[\s*["{\]]')
|
|
102
123
|
|
|
103
124
|
VALID_MODES = {"interactive", "headless"}
|
|
104
125
|
VALID_SOURCE_TYPES = {None, "source", "docs-only"}
|
|
@@ -115,8 +136,12 @@ def _find_registry_files(tree: list[str]) -> list[str]:
|
|
|
115
136
|
for path in tree:
|
|
116
137
|
if not isinstance(path, str):
|
|
117
138
|
continue
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
parts = path.split("/")
|
|
140
|
+
# Never a path outside the repository: --registry-files feeds a shell
|
|
141
|
+
# loop that writes each file under a folder.
|
|
142
|
+
if path.startswith("/") or ".." in parts:
|
|
143
|
+
continue
|
|
144
|
+
if parts[-1] in {"registry.ts", "components.ts", "registry.tsx", "components.tsx"}:
|
|
120
145
|
hits.append(path)
|
|
121
146
|
return hits
|
|
122
147
|
|
|
@@ -186,67 +211,90 @@ def _component_registry_match(
|
|
|
186
211
|
if isinstance(path, str) and isinstance(content, str):
|
|
187
212
|
contents[path] = content
|
|
188
213
|
|
|
189
|
-
#
|
|
214
|
+
# A registry file with contents is judged by them; one without contents
|
|
215
|
+
# counts by its presence in headless mode only.
|
|
216
|
+
not_given: list[str] = []
|
|
190
217
|
for path in registry_paths:
|
|
191
|
-
if path in contents:
|
|
192
|
-
|
|
193
|
-
has_annotation = _has_component_array_annotation(contents[path])
|
|
194
|
-
if entry_count >= 10 or has_annotation:
|
|
195
|
-
return {
|
|
196
|
-
"registry_path": path,
|
|
197
|
-
"entry_count": entry_count if entry_count >= 10 else None,
|
|
198
|
-
"component_array_annotation": has_annotation,
|
|
199
|
-
"contents_inspected": True,
|
|
200
|
-
}
|
|
201
|
-
# File present but neither threshold met — content disqualifies
|
|
218
|
+
if path not in contents:
|
|
219
|
+
not_given.append(path)
|
|
202
220
|
continue
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
entry_count = _registry_entry_count(contents[path])
|
|
222
|
+
has_annotation = _has_component_array_annotation(contents[path])
|
|
223
|
+
if entry_count >= 10 or has_annotation:
|
|
224
|
+
return {
|
|
225
|
+
"registry_path": path,
|
|
226
|
+
"entry_count": entry_count if entry_count >= 10 else None,
|
|
227
|
+
"component_array_annotation": has_annotation,
|
|
228
|
+
"contents_inspected": True,
|
|
229
|
+
}
|
|
230
|
+
# Neither threshold met: the contents disqualify this file
|
|
231
|
+
|
|
232
|
+
if mode == "headless" and not_given:
|
|
206
233
|
return {
|
|
207
|
-
"registry_path":
|
|
234
|
+
"registry_path": not_given[0],
|
|
208
235
|
"entry_count": None,
|
|
209
236
|
"component_array_annotation": False,
|
|
210
237
|
"contents_inspected": False,
|
|
211
238
|
}
|
|
212
|
-
# Interactive without contents
|
|
239
|
+
# Interactive without contents: the rule does not match
|
|
213
240
|
return None
|
|
214
241
|
|
|
215
242
|
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
def _read_signals(payload: dict) -> dict:
|
|
244
|
+
"""The caller's classified intent signals, checked for shape."""
|
|
245
|
+
signals = payload.get("signals")
|
|
246
|
+
wanted = ", ".join(SIGNAL_KEYS)
|
|
247
|
+
if not isinstance(signals, dict):
|
|
248
|
+
_die(f"signals must be an object with {wanted}, classified from the user's intent; got {signals!r}")
|
|
249
|
+
unknown = sorted(set(signals) - set(SIGNAL_KEYS))
|
|
250
|
+
missing = [key for key in SIGNAL_KEYS if key not in signals]
|
|
251
|
+
if unknown or missing:
|
|
252
|
+
_die(f"signals must have exactly {wanted}; unknown {unknown}, missing {missing}")
|
|
253
|
+
for key in ("wants_wiring_pattern", "wants_narrow_api"):
|
|
254
|
+
if not isinstance(signals[key], bool):
|
|
255
|
+
_die(f"signals.{key} must be true or false; got {signals[key]!r}")
|
|
256
|
+
names = signals["named_module_subset"]
|
|
257
|
+
if not isinstance(names, list) or not all(isinstance(n, str) and n.strip() for n in names):
|
|
258
|
+
_die(f"signals.named_module_subset must be a list of module names ([] when the intent names none); got {names!r}")
|
|
259
|
+
return {
|
|
260
|
+
"wants_wiring_pattern": signals["wants_wiring_pattern"],
|
|
261
|
+
"named_module_subset": [n.strip() for n in names],
|
|
262
|
+
"wants_narrow_api": signals["wants_narrow_api"],
|
|
263
|
+
}
|
|
221
264
|
|
|
222
265
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return ("specific-modules-naming", {"phrases": naming_hits})
|
|
231
|
-
if module_count >= 6:
|
|
232
|
-
return ("specific-modules-count", {"module_count": module_count})
|
|
233
|
-
return None
|
|
266
|
+
def _count(payload: dict, key: str) -> int:
|
|
267
|
+
value = payload.get(key)
|
|
268
|
+
if value is None:
|
|
269
|
+
return 0
|
|
270
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
271
|
+
_die(f"{key} must be a whole number >= 0; got {value!r}")
|
|
272
|
+
return value
|
|
234
273
|
|
|
235
274
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
275
|
+
def _specific_modules_match(named: list[str], module_count: int) -> tuple[str, dict] | None:
|
|
276
|
+
if named:
|
|
277
|
+
return ("specific-modules-naming", {"named_module_subset": named})
|
|
278
|
+
if module_count >= 6:
|
|
279
|
+
return ("specific-modules-count", {"module_count": module_count})
|
|
240
280
|
return None
|
|
241
281
|
|
|
242
282
|
|
|
243
283
|
def recommend(payload: dict) -> dict:
|
|
244
|
-
"""Apply the
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
284
|
+
"""Apply the ladder. Always returns a recommendation."""
|
|
285
|
+
for key in FREE_TEXT_KEYS:
|
|
286
|
+
if key in payload:
|
|
287
|
+
_die(f"{key} is free text, which this script no longer reads: classify it into signals "
|
|
288
|
+
f"({', '.join(SIGNAL_KEYS)}) and pass those")
|
|
289
|
+
unknown = sorted(set(payload) - PAYLOAD_KEYS)
|
|
290
|
+
if unknown:
|
|
291
|
+
_die(f"unknown payload keys {unknown}; expected {sorted(PAYLOAD_KEYS)}")
|
|
292
|
+
|
|
293
|
+
tree = payload.get("tree")
|
|
294
|
+
if tree is None:
|
|
295
|
+
tree = []
|
|
296
|
+
if not isinstance(tree, list):
|
|
297
|
+
_die(f"tree must be a list of file paths; got {type(tree).__name__}")
|
|
250
298
|
entry_files = payload.get("entry_files")
|
|
251
299
|
source_type = payload.get("source_type")
|
|
252
300
|
mode = payload.get("mode") or "headless"
|
|
@@ -255,17 +303,23 @@ def recommend(payload: dict) -> dict:
|
|
|
255
303
|
_die(f"source_type must be one of {sorted(t for t in VALID_SOURCE_TYPES if t)} or null; got {source_type!r}")
|
|
256
304
|
if mode not in VALID_MODES:
|
|
257
305
|
_die(f"mode must be one of {sorted(VALID_MODES)}; got {mode!r}")
|
|
306
|
+
module_count = _count(payload, "module_count")
|
|
307
|
+
export_count = _count(payload, "export_count")
|
|
258
308
|
|
|
259
|
-
# Short-circuit: docs-only has no source surface to scope
|
|
309
|
+
# Short-circuit: docs-only has no source surface to scope, and needs no signals
|
|
260
310
|
if source_type == "docs-only":
|
|
311
|
+
if "signals" in payload:
|
|
312
|
+
_read_signals(payload)
|
|
261
313
|
return {
|
|
262
314
|
"scope_type": "docs-only",
|
|
263
315
|
"matched_heuristic": "docs-only-shortcircuit",
|
|
264
316
|
"signals": {"source_type": "docs-only"},
|
|
265
|
-
"rationale": "source_type is docs-only
|
|
317
|
+
"rationale": "Docs Only because source_type is docs-only: there is no source surface to scope, so the brief uses the docs-only template.",
|
|
266
318
|
}
|
|
267
319
|
|
|
268
|
-
|
|
320
|
+
signals = _read_signals(payload)
|
|
321
|
+
|
|
322
|
+
# Rule 1: component-library
|
|
269
323
|
cr = _component_registry_match(tree, entry_files, mode)
|
|
270
324
|
if cr:
|
|
271
325
|
rationale_bits = [f"a component registry was detected at {cr['registry_path']}"]
|
|
@@ -274,7 +328,7 @@ def recommend(payload: dict) -> dict:
|
|
|
274
328
|
if cr.get("component_array_annotation"):
|
|
275
329
|
rationale_bits.append("and a Component[] type annotation")
|
|
276
330
|
if not cr.get("contents_inspected"):
|
|
277
|
-
rationale_bits.append("(presence-only match
|
|
331
|
+
rationale_bits.append("(presence-only match: file contents not inspected in headless mode)")
|
|
278
332
|
return {
|
|
279
333
|
"scope_type": "component-library",
|
|
280
334
|
"matched_heuristic": "component-registry",
|
|
@@ -282,45 +336,40 @@ def recommend(payload: dict) -> dict:
|
|
|
282
336
|
"rationale": "Component Library because " + " ".join(rationale_bits) + ".",
|
|
283
337
|
}
|
|
284
338
|
|
|
285
|
-
# Rule 2
|
|
286
|
-
|
|
287
|
-
if ra:
|
|
288
|
-
kw_str = ", ".join(f"'{k}'" for k in ra["keywords"])
|
|
339
|
+
# Rule 2: reference-app
|
|
340
|
+
if signals["wants_wiring_pattern"]:
|
|
289
341
|
return {
|
|
290
342
|
"scope_type": "reference-app",
|
|
291
|
-
"matched_heuristic": "reference-app-
|
|
292
|
-
"signals":
|
|
293
|
-
"rationale":
|
|
343
|
+
"matched_heuristic": "reference-app-intent",
|
|
344
|
+
"signals": {"wants_wiring_pattern": True},
|
|
345
|
+
"rationale": "Reference App because the intent asks how an app, starter or example wires its parts together, a wiring-pattern skill rather than a library API.",
|
|
294
346
|
}
|
|
295
347
|
|
|
296
|
-
# Rule 3
|
|
297
|
-
sm = _specific_modules_match(
|
|
348
|
+
# Rule 3: specific-modules
|
|
349
|
+
sm = _specific_modules_match(signals["named_module_subset"], module_count)
|
|
298
350
|
if sm:
|
|
299
|
-
heuristic,
|
|
351
|
+
heuristic, fired = sm
|
|
300
352
|
if heuristic == "specific-modules-naming":
|
|
301
|
-
|
|
302
|
-
rationale = f"Specific Modules because the intent names a subset ({phrases})."
|
|
353
|
+
rationale = f"Specific Modules because the intent limits the skill to {', '.join(fired['named_module_subset'])}."
|
|
303
354
|
else:
|
|
304
|
-
rationale = f"Specific Modules because the analysis surfaced {
|
|
355
|
+
rationale = f"Specific Modules because the analysis surfaced {fired['module_count']} top-level modules, likely too many for a single cohesive scope."
|
|
305
356
|
return {
|
|
306
357
|
"scope_type": "specific-modules",
|
|
307
358
|
"matched_heuristic": heuristic,
|
|
308
|
-
"signals":
|
|
359
|
+
"signals": fired,
|
|
309
360
|
"rationale": rationale,
|
|
310
361
|
}
|
|
311
362
|
|
|
312
|
-
# Rule 4
|
|
313
|
-
|
|
314
|
-
if pa:
|
|
315
|
-
kw_str = ", ".join(f"'{k}'" for k in pa["keywords"])
|
|
363
|
+
# Rule 4: narrow public API
|
|
364
|
+
if signals["wants_narrow_api"] and 0 < export_count <= 8:
|
|
316
365
|
return {
|
|
317
366
|
"scope_type": "public-api",
|
|
318
367
|
"matched_heuristic": "narrow-public-api",
|
|
319
|
-
"signals":
|
|
320
|
-
"rationale": f"Public API Only because the intent
|
|
368
|
+
"signals": {"wants_narrow_api": True, "export_count": export_count},
|
|
369
|
+
"rationale": f"Public API Only because the intent asks for the public API only and the manifest exposes {export_count} named exports, a clear narrow public surface.",
|
|
321
370
|
}
|
|
322
371
|
|
|
323
|
-
# Rule 5
|
|
372
|
+
# Rule 5: fallback
|
|
324
373
|
return {
|
|
325
374
|
"scope_type": "full-library",
|
|
326
375
|
"matched_heuristic": "default-full-library",
|
|
@@ -328,19 +377,118 @@ def recommend(payload: dict) -> dict:
|
|
|
328
377
|
"module_count": module_count,
|
|
329
378
|
"export_count": export_count,
|
|
330
379
|
},
|
|
331
|
-
"rationale": "Full Library
|
|
380
|
+
"rationale": "Full Library because no signal matched a narrower scope, so the default is to cover everything.",
|
|
332
381
|
}
|
|
333
382
|
|
|
334
383
|
|
|
335
|
-
def
|
|
384
|
+
def _parse_tree_listing(text: str, source: str) -> list[str]:
|
|
385
|
+
"""The file paths a --tree-file listing names: JSON, or one path per line."""
|
|
386
|
+
stripped = text.strip()
|
|
387
|
+
if not stripped:
|
|
388
|
+
_die(f"the tree listing {source} is empty; the command that wrote it may have failed")
|
|
389
|
+
if not JSON_LISTING_RE.match(stripped):
|
|
390
|
+
paths = []
|
|
391
|
+
for line in stripped.splitlines():
|
|
392
|
+
path = line.strip().removeprefix("./")
|
|
393
|
+
if path:
|
|
394
|
+
paths.append(path)
|
|
395
|
+
return paths
|
|
396
|
+
try:
|
|
397
|
+
data = json.loads(stripped)
|
|
398
|
+
except json.JSONDecodeError as e:
|
|
399
|
+
_die(f"the tree listing {source} is not valid JSON: {e}")
|
|
400
|
+
entries = data
|
|
401
|
+
if isinstance(data, dict):
|
|
402
|
+
status = data.get("status")
|
|
403
|
+
if status is not None and status != "ok":
|
|
404
|
+
_die(f"the tree listing {source} reports a failure ({status}): {data.get('message') or 'no message'}")
|
|
405
|
+
entries = data.get("tree")
|
|
406
|
+
if not isinstance(entries, list):
|
|
407
|
+
_die(f"the tree listing {source} holds no list of paths (a `tree` list, or a JSON list)")
|
|
408
|
+
paths = []
|
|
409
|
+
for entry in entries:
|
|
410
|
+
if isinstance(entry, str):
|
|
411
|
+
paths.append(entry)
|
|
412
|
+
elif isinstance(entry, dict) and isinstance(entry.get("path"), str) and entry.get("type", "blob") == "blob":
|
|
413
|
+
paths.append(entry["path"])
|
|
414
|
+
return paths
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _read_tree_file(value: str) -> list[str]:
|
|
418
|
+
if value == "-":
|
|
419
|
+
return _parse_tree_listing(sys.stdin.read(), "on stdin")
|
|
420
|
+
try:
|
|
421
|
+
with open(value, encoding="utf-8-sig") as fh:
|
|
422
|
+
text = fh.read()
|
|
423
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
424
|
+
_die(f"cannot read --tree-file {value}: {getattr(e, 'strerror', None) or e}")
|
|
425
|
+
return _parse_tree_listing(text, f"in {value}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _read_entry_dir(folder: str, tree: object) -> list[dict]:
|
|
429
|
+
"""The registry files the tree lists, read from `folder` laid out like the repository."""
|
|
430
|
+
files = []
|
|
431
|
+
for path in _find_registry_files(tree if isinstance(tree, list) else []):
|
|
432
|
+
try:
|
|
433
|
+
with open(os.path.join(folder, *path.split("/")), encoding="utf-8", errors="replace") as fh:
|
|
434
|
+
files.append({"path": path, "content": fh.read()})
|
|
435
|
+
except OSError:
|
|
436
|
+
continue # not fetched: its contents were not given
|
|
437
|
+
return files
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _read_export_count(value: str) -> int:
|
|
441
|
+
"""The number of distinct export names an extractor result lists."""
|
|
442
|
+
try:
|
|
443
|
+
with open(value, encoding="utf-8-sig") as fh:
|
|
444
|
+
data = json.load(fh)
|
|
445
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
446
|
+
_die(f"cannot read --extract-file {value}: {getattr(e, 'strerror', None) or e}")
|
|
447
|
+
except json.JSONDecodeError as e:
|
|
448
|
+
_die(f"--extract-file {value} is not valid JSON: {e}")
|
|
449
|
+
exports = data.get("exports") if isinstance(data, dict) else None
|
|
450
|
+
if not isinstance(exports, list):
|
|
451
|
+
_die(f"--extract-file {value} holds no `exports` list (skf-extract-public-api.py output)")
|
|
452
|
+
return len({e["name"] for e in exports if isinstance(e, dict) and isinstance(e.get("name"), str) and e["name"]})
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|
336
456
|
parser = argparse.ArgumentParser(
|
|
337
|
-
description="Recommend a scope type
|
|
457
|
+
description="Recommend a scope type: the documented ladder over facts and classified intent signals.",
|
|
338
458
|
)
|
|
339
459
|
parser.add_argument(
|
|
340
460
|
"--json",
|
|
341
461
|
help="JSON payload (alternative to stdin)",
|
|
342
462
|
)
|
|
463
|
+
parser.add_argument(
|
|
464
|
+
"--tree-file",
|
|
465
|
+
help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin",
|
|
466
|
+
)
|
|
467
|
+
parser.add_argument(
|
|
468
|
+
"--entry-dir",
|
|
469
|
+
help="a folder laid out like the repository that holds the registry files the tree lists (read as entry_files)",
|
|
470
|
+
)
|
|
471
|
+
parser.add_argument(
|
|
472
|
+
"--extract-file",
|
|
473
|
+
help="the extractor's output (skf-extract-public-api.py): its distinct export names are counted as export_count",
|
|
474
|
+
)
|
|
475
|
+
parser.add_argument(
|
|
476
|
+
"--registry-files",
|
|
477
|
+
action="store_true",
|
|
478
|
+
help="print the registry files the --tree-file listing holds, one path per line, and exit",
|
|
479
|
+
)
|
|
343
480
|
args = parser.parse_args(argv)
|
|
481
|
+
if args.registry_files:
|
|
482
|
+
if args.tree_file is None:
|
|
483
|
+
_die("--registry-files lists the registry files of --tree-file, so pass it")
|
|
484
|
+
if args.json is not None or args.entry_dir is not None or args.extract_file is not None:
|
|
485
|
+
_die("--registry-files reads only --tree-file")
|
|
486
|
+
elif args.tree_file == "-" and args.json is None:
|
|
487
|
+
_die("--tree-file - reads the tree from stdin, so pass the payload with --json")
|
|
488
|
+
return args
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _read_payload(args: argparse.Namespace) -> dict:
|
|
344
492
|
if args.json is not None:
|
|
345
493
|
raw = args.json
|
|
346
494
|
else:
|
|
@@ -353,6 +501,18 @@ def _parse_argv(argv: list[str]) -> dict:
|
|
|
353
501
|
_die(f"invalid JSON input: {e}")
|
|
354
502
|
if not isinstance(payload, dict):
|
|
355
503
|
_die("payload must be a JSON object")
|
|
504
|
+
if args.tree_file is not None:
|
|
505
|
+
if "tree" in payload:
|
|
506
|
+
_die("pass the tree once: in the payload or with --tree-file, not both")
|
|
507
|
+
payload["tree"] = _read_tree_file(args.tree_file)
|
|
508
|
+
if args.entry_dir is not None:
|
|
509
|
+
if "entry_files" in payload:
|
|
510
|
+
_die("pass the registry contents once: as entry_files in the payload or with --entry-dir, not both")
|
|
511
|
+
payload["entry_files"] = _read_entry_dir(args.entry_dir, payload.get("tree"))
|
|
512
|
+
if args.extract_file is not None:
|
|
513
|
+
if "export_count" in payload:
|
|
514
|
+
_die("pass the export count once: as export_count in the payload or with --extract-file, not both")
|
|
515
|
+
payload["export_count"] = _read_export_count(args.extract_file)
|
|
356
516
|
return payload
|
|
357
517
|
|
|
358
518
|
|
|
@@ -377,7 +537,14 @@ def _force_utf8(*streams) -> None:
|
|
|
377
537
|
|
|
378
538
|
def main(argv: list[str]) -> int:
|
|
379
539
|
_force_utf8(sys.stdin, sys.stdout)
|
|
380
|
-
|
|
540
|
+
args = _parse_args(argv)
|
|
541
|
+
if args.registry_files:
|
|
542
|
+
paths = _find_registry_files(_read_tree_file(args.tree_file))
|
|
543
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
544
|
+
sys.stdout.reconfigure(newline="\n") # a shell loop reads the lines: no CR on Windows
|
|
545
|
+
sys.stdout.write("".join(f"{path}\n" for path in paths))
|
|
546
|
+
return 0
|
|
547
|
+
payload = _read_payload(args)
|
|
381
548
|
result = recommend(payload)
|
|
382
549
|
json.dump(result, sys.stdout, ensure_ascii=False)
|
|
383
550
|
sys.stdout.write("\n")
|