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,5 +1,5 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Detect Language — deterministic primary-language detection from a flat file tree.
|
|
@@ -19,8 +19,9 @@ Detection rules (apply in order, first match wins):
|
|
|
19
19
|
"python-multi-package" → python (high)
|
|
20
20
|
A non-JS workspace root's language wins over a nested package.json +
|
|
21
21
|
tsconfig.json (e.g. a docs/ or website/ site that is not a workspace
|
|
22
|
-
member). JS-family workspaces (npm/pnpm/lerna) carry no entry
|
|
23
|
-
fall through to rule 1, whose
|
|
22
|
+
member). JS-family workspaces (npm/pnpm/lerna/rush/nx) carry no entry
|
|
23
|
+
here and fall through to rule 1, whose package.json files correctly
|
|
24
|
+
resolve js/ts.
|
|
24
25
|
1. package.json (with optional tsconfig.json companion):
|
|
25
26
|
tsconfig.json present → typescript (high)
|
|
26
27
|
tsconfig.json absent → javascript (high)
|
|
@@ -29,26 +30,70 @@ Detection rules (apply in order, first match wins):
|
|
|
29
30
|
4. go.mod → go (high)
|
|
30
31
|
5. pom.xml → java (high)
|
|
31
32
|
6. build.gradle.kts → kotlin (high)
|
|
32
|
-
7.
|
|
33
|
+
7. Package.swift → swift (high)
|
|
34
|
+
8. Gemfile → ruby (high)
|
|
35
|
+
9. build.gradle (Groovy), the tree decides:
|
|
33
36
|
src/main/kotlin/ → kotlin (medium)
|
|
34
37
|
else → java (medium)
|
|
35
|
-
|
|
36
|
-
9. *.csproj | *.sln → csharp (high)
|
|
37
|
-
10. Gemfile → ruby (high)
|
|
38
|
+
10. *.csproj | *.sln → csharp (high)
|
|
38
39
|
11. Extension-frequency fallback over the full tree.
|
|
39
40
|
dominant extension >= 50% of code files → that language (medium)
|
|
40
41
|
no clear winner → unknown (low)
|
|
41
42
|
|
|
43
|
+
Which manifests decide (rules 1-10). The tree's root is the deepest folder
|
|
44
|
+
that holds every path: the repository root for a whole listing, a unit's
|
|
45
|
+
own folder for the files of one unit. A manifest in a hidden folder or in
|
|
46
|
+
a folder _NON_CORE_PATH_SEGMENTS names (docs, examples, tests, fixtures,
|
|
47
|
+
benchmarks, scripts, tools, ...) below that root is not the project's own
|
|
48
|
+
package, so it decides only when no other manifest exists. The rules then
|
|
49
|
+
run over the shallowest of the remaining manifests first: the manifests of
|
|
50
|
+
the least deep folder that holds one decide, in rule order, so a root
|
|
51
|
+
pyproject.toml beats docs/package.json and pydantic-core/Cargo.toml. A
|
|
52
|
+
manifest at the root decides at its rule's confidence. When none sits at
|
|
53
|
+
the root (CPython's listing holds only Platforms/ and PCbuild/ ones), the
|
|
54
|
+
shallowest manifest below it decides at medium confidence, and
|
|
55
|
+
detected_languages ends with source_language, the language most of the
|
|
56
|
+
tree's source files are written in, so the caller sees both answers. The
|
|
57
|
+
tsconfig.json check reads every core folder (every folder when no
|
|
58
|
+
manifest is core), so a docs site's tsconfig.json does not make a root
|
|
59
|
+
package.json typescript.
|
|
60
|
+
|
|
42
61
|
CLI:
|
|
43
62
|
echo '{"tree": ["path1", "path2", ...]}' | uv run skf-detect-language.py
|
|
44
63
|
uv run skf-detect-language.py --json '{"tree": [...]}'
|
|
64
|
+
uv run skf-detect-language.py --tree-file <file> [--workspace-signal <manifest_kind>]
|
|
65
|
+
<listing> | uv run skf-detect-language.py --tree-file - [--workspace-signal <manifest_kind>]
|
|
66
|
+
uv run skf-detect-language.py scope-patterns --tree-file <file> --language <language>
|
|
67
|
+
|
|
68
|
+
--tree-file reads the repository's file list from a file (`-` reads it from
|
|
69
|
+
stdin), so a tree of any size reaches the script whole and never passes
|
|
70
|
+
through a shell string or the model:
|
|
71
|
+
- JSON: an object whose `tree` lists the paths, such as the output of
|
|
72
|
+
skf-github-probe.py tree (a listing whose `status` is not "ok" is
|
|
73
|
+
refused, with its `message`) or a GitHub git/trees response (only its
|
|
74
|
+
`blob` entries count); or a JSON list of paths.
|
|
75
|
+
- Anything else: one path per line, as `git -c core.quotePath=false
|
|
76
|
+
ls-files` prints them (a leading `./`, as `find .` prints it, is
|
|
77
|
+
dropped).
|
|
78
|
+
A byte order mark before the listing is dropped, and a listing that holds
|
|
79
|
+
no path is refused. These are the listings skf-recommend-scope-type.py's
|
|
80
|
+
--tree-file reads, which takes `[]` as its deliberate fallback.
|
|
81
|
+
read_tree_file() is the reader skf-detect-workspaces.py and
|
|
82
|
+
skf-shape-detect.py load for their own --tree-file. With --tree-file, stdin
|
|
83
|
+
is never read for the payload, so a payload piped in beside a --tree-file
|
|
84
|
+
<file> is not seen: pass workspace_signal with --workspace-signal (or the
|
|
85
|
+
payload with --json, which may not carry `tree` as well).
|
|
86
|
+
--workspace-signal sets the payload's workspace_signal in any form; passing
|
|
87
|
+
it in the payload as well is an error.
|
|
45
88
|
|
|
46
89
|
Input (JSON object on stdin or via --json):
|
|
47
|
-
tree
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
90
|
+
tree list of repo-relative file paths (required, non-empty;
|
|
91
|
+
or --tree-file)
|
|
92
|
+
workspace_signal optional manifest_kind from skf-detect-workspaces (or
|
|
93
|
+
--workspace-signal). When it names a non-JS workspace
|
|
94
|
+
("cargo-workspace", "python-multi-package"), rule 0
|
|
95
|
+
returns the root language and ignores nested
|
|
96
|
+
package.json/tsconfig matches.
|
|
52
97
|
|
|
53
98
|
Output (JSON on stdout):
|
|
54
99
|
language — javascript | typescript | rust | python | go
|
|
@@ -56,38 +101,85 @@ Output (JSON on stdout):
|
|
|
56
101
|
| unknown
|
|
57
102
|
confidence — "high" | "medium" | "low"
|
|
58
103
|
detection_source — human-readable string naming what fired (manifest
|
|
59
|
-
basename, extension share, etc.)
|
|
60
|
-
|
|
104
|
+
basename, extension share, etc.); a manifest below
|
|
105
|
+
the root is named by its path
|
|
106
|
+
fallback_to_extension_frequency: bool (true when rule 11 fired)
|
|
107
|
+
source_language: the extension-frequency answer over the full tree
|
|
108
|
+
(rule 11's language, or unknown), whatever decided
|
|
109
|
+
`language`: the language most source files are in
|
|
61
110
|
detected_languages — ordered, deduplicated list of EVERY manifest-level
|
|
62
|
-
match
|
|
111
|
+
match, ranked as the winner walk ranks them (folder
|
|
112
|
+
depth, then rule order), then the languages of the
|
|
113
|
+
manifests that never decide (non-core folders), then
|
|
114
|
+
source_language when no manifest sits at the root.
|
|
63
115
|
language == detected_languages[0] whenever the manifest
|
|
64
116
|
table fired, so a caller can auto-pick detected_languages[0]
|
|
65
117
|
and gate multi-language disambiguation on
|
|
66
118
|
len(detected_languages) > 1. Special cases: a
|
|
67
119
|
workspace_signal override (rule 0) is decisive and returns
|
|
68
120
|
a single-element [language]; the extension-frequency
|
|
69
|
-
fallback (rule
|
|
121
|
+
fallback (rule 11) contributes [language] for a best guess
|
|
70
122
|
or [] when language is "unknown".
|
|
71
123
|
|
|
72
124
|
Exit codes:
|
|
73
|
-
0
|
|
74
|
-
2
|
|
125
|
+
0 recommendation produced (even when language is "unknown")
|
|
126
|
+
2 bad input: invalid JSON, a missing or empty tree, the tree or the
|
|
127
|
+
workspace_signal passed twice, or a tree listing that cannot be read,
|
|
128
|
+
holds no path or reports a failure
|
|
129
|
+
|
|
130
|
+
scope-patterns prints a brief's `scope.include` and `scope.exclude` for one
|
|
131
|
+
language (a value `language` above takes) from the --tree-file listing alone,
|
|
132
|
+
so a manifests-only sparse checkout, which holds no source folder on disk,
|
|
133
|
+
roots them the same as a full one. The paths are read below the tree's root
|
|
134
|
+
(above), and the patterns carry that root, so the files of one unit give
|
|
135
|
+
patterns rooted at the unit's folder. The source folders are the first of
|
|
136
|
+
these to hold files of the language's extensions (_SCOPE_RULES) and leave
|
|
137
|
+
no package out:
|
|
138
|
+
1. the language's conventional folders, in order (src/ then lib/ for
|
|
139
|
+
JavaScript and TypeScript; `**/src/main/java` is every module's). A
|
|
140
|
+
file counts when no folder down to the conventional one is hidden or
|
|
141
|
+
non-core, whatever the folders below it are named (a Java package
|
|
142
|
+
path such as com/example/);
|
|
143
|
+
2. for Python, every top-level core folder that holds an __init__.py
|
|
144
|
+
itself: the import packages (sklearn/ of scikit-learn, django/);
|
|
145
|
+
3. otherwise the whole tree, less each top-level hidden or non-core
|
|
146
|
+
folder that holds files of the language. Go always takes the whole
|
|
147
|
+
tree, and `unknown` matches every file.
|
|
148
|
+
A folder leaves a package out when a core folder below the root that holds
|
|
149
|
+
a manifest (not a vendored copy) has files of the language outside it, in
|
|
150
|
+
its own files or a core folder of its own: a monorepo merged into one
|
|
151
|
+
skill, whose root lib/ is one package among its members (aws-sdk-js-v3,
|
|
152
|
+
with clients/ and packages/ beside it). Then only a later conventional
|
|
153
|
+
folder that matches at any depth (`**/src/main/java`) may still answer,
|
|
154
|
+
and otherwise the whole tree does.
|
|
155
|
+
`include` lists `<folder>/**/*<ext>` for each extension the source folders
|
|
156
|
+
hold (every extension of the language when they hold none); `exclude` lists
|
|
157
|
+
the language's test, benchmark and vendored-code globs, then the left-out
|
|
158
|
+
folders. Output: {language, source_folders, include, exclude}, with
|
|
159
|
+
`source_folders` repo-relative (`.` for the repository root). Exit 2 on an
|
|
160
|
+
unusable listing or a language outside the list.
|
|
75
161
|
"""
|
|
76
162
|
|
|
77
163
|
from __future__ import annotations
|
|
78
164
|
|
|
79
165
|
import argparse
|
|
80
166
|
import json
|
|
167
|
+
import re
|
|
81
168
|
import sys
|
|
82
169
|
from collections import Counter
|
|
83
170
|
from typing import Any
|
|
84
171
|
|
|
172
|
+
# A --tree-file listing is JSON when it opens like a JSON object or list of
|
|
173
|
+
# paths. A bare leading bracket is not enough: a line listing can start with
|
|
174
|
+
# a path such as `[slug]/page.tsx`.
|
|
175
|
+
JSON_LISTING_RE = re.compile(r'\{\s*["}]|\[\s*["{\]]')
|
|
176
|
+
|
|
85
177
|
# Workspace manifest_kind → root language (rule 0). Only non-JS workspace kinds
|
|
86
178
|
# appear here: a Cargo/Python workspace root is unambiguously rust/python, and a
|
|
87
179
|
# nested package.json+tsconfig (a docs or website subproject) must not win. JS
|
|
88
|
-
# workspace kinds (npm-workspaces/pnpm-workspaces/lerna) are
|
|
89
|
-
# absent
|
|
90
|
-
# correctly. generic-folders / null carry no language signal.
|
|
180
|
+
# workspace kinds (npm-workspaces/pnpm-workspaces/lerna/rush/nx) are
|
|
181
|
+
# intentionally absent: their repositories hold package.json files, so rule 1
|
|
182
|
+
# resolves js/ts correctly. generic-folders / null carry no language signal.
|
|
91
183
|
_WORKSPACE_SIGNAL_LANGUAGE: dict[str, str] = {
|
|
92
184
|
"cargo-workspace": "rust",
|
|
93
185
|
"python-multi-package": "python",
|
|
@@ -140,6 +232,19 @@ _EXTENSION_TO_LANGUAGE: dict[str, str] = {
|
|
|
140
232
|
|
|
141
233
|
_DOMINANCE_THRESHOLD = 0.50
|
|
142
234
|
|
|
235
|
+
# Folders whose manifests are not the project's own package: a docs or
|
|
236
|
+
# website site, an example, a test fixture, a benchmark, a script or a tool.
|
|
237
|
+
# A copy of skf-shape-detect.py's _NON_CORE_PATH_SEGMENTS, which cannot be
|
|
238
|
+
# imported from there: skf-shape-detect.py loads this script. A hidden folder
|
|
239
|
+
# (.github/, .devcontainer/) is non-core too.
|
|
240
|
+
_NON_CORE_PATH_SEGMENTS = frozenset({
|
|
241
|
+
"example", "examples", "demo", "demos", "sample", "samples",
|
|
242
|
+
"playground", "playgrounds", "e2e", "benchmark", "benchmarks", "bench",
|
|
243
|
+
"fixture", "fixtures", "website", "websites", "www",
|
|
244
|
+
"docs", "doc", "scripts", "tools", "tooling", "devtools", "dev-tools",
|
|
245
|
+
"test", "tests", "__tests__", "integration", "smoke",
|
|
246
|
+
})
|
|
247
|
+
|
|
143
248
|
|
|
144
249
|
def _die(message: str, code: int = 2) -> None:
|
|
145
250
|
sys.stderr.write(f"skf-detect-language: {message}\n")
|
|
@@ -171,6 +276,12 @@ def _extension(path: str) -> str:
|
|
|
171
276
|
return "." + base.rsplit(".", 1)[-1].lower()
|
|
172
277
|
|
|
173
278
|
|
|
279
|
+
def is_source_file(path: str) -> bool:
|
|
280
|
+
"""Whether a path is a source file the extension-frequency fallback
|
|
281
|
+
counts (skf-detect-workspaces.py's tree snapshot counts the same)."""
|
|
282
|
+
return _extension(path) in _EXTENSION_TO_LANGUAGE
|
|
283
|
+
|
|
284
|
+
|
|
174
285
|
def _frequency_fallback(tree: list[str]) -> dict[str, Any]:
|
|
175
286
|
"""Score the tree by source-extension frequency. Returns the result envelope."""
|
|
176
287
|
counter: Counter[str] = Counter()
|
|
@@ -211,18 +322,119 @@ def _frequency_fallback(tree: list[str]) -> dict[str, Any]:
|
|
|
211
322
|
}
|
|
212
323
|
|
|
213
324
|
|
|
325
|
+
def _parts(path: str) -> list[str]:
|
|
326
|
+
"""The segments of a repo-relative path, without empty and `.` ones."""
|
|
327
|
+
return [p for p in path.split("/") if p and p != "."]
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _tree_root(tree: list[str]) -> list[str]:
|
|
331
|
+
"""The folder segments every path of the tree sits under (the tree's root)."""
|
|
332
|
+
root: list[str] | None = None
|
|
333
|
+
for path in tree:
|
|
334
|
+
folders = _parts(path)[:-1]
|
|
335
|
+
if root is None:
|
|
336
|
+
root = folders
|
|
337
|
+
continue
|
|
338
|
+
depth = 0
|
|
339
|
+
while depth < min(len(root), len(folders)) and root[depth] == folders[depth]:
|
|
340
|
+
depth += 1
|
|
341
|
+
root = root[:depth]
|
|
342
|
+
if not root:
|
|
343
|
+
break
|
|
344
|
+
return root or []
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _is_core(folders: list[str]) -> bool:
|
|
348
|
+
"""Whether no folder below the tree's root is hidden or a non-core folder."""
|
|
349
|
+
return not any(seg.startswith(".") or seg.lower() in _NON_CORE_PATH_SEGMENTS for seg in folders)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _rule_of(basename: str) -> int | None:
|
|
353
|
+
"""The rank of the table rule a manifest basename fires, in rule order
|
|
354
|
+
(package.json first, the suffix rules last), or None."""
|
|
355
|
+
if basename == "package.json":
|
|
356
|
+
return 0
|
|
357
|
+
for i, (name, _language, _source) in enumerate(_MANIFEST_RULES):
|
|
358
|
+
if basename == name:
|
|
359
|
+
return 1 + i
|
|
360
|
+
if basename == "build.gradle":
|
|
361
|
+
return 1 + len(_MANIFEST_RULES)
|
|
362
|
+
for i, (suffix, _language, _source) in enumerate(_SUFFIX_RULES):
|
|
363
|
+
if basename.endswith(suffix):
|
|
364
|
+
return 2 + len(_MANIFEST_RULES) + i
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _rule_result(rank: int, context: dict[str, bool]) -> dict[str, Any]:
|
|
369
|
+
"""The result a table rule gives; `context` says whether the paths its
|
|
370
|
+
checks read hold a tsconfig.json and a src/main/kotlin/ folder."""
|
|
371
|
+
if rank == 0:
|
|
372
|
+
if context["tsconfig"]:
|
|
373
|
+
return {"language": "typescript", "confidence": "high",
|
|
374
|
+
"detection_source": "package.json + tsconfig.json present"}
|
|
375
|
+
return {"language": "javascript", "confidence": "high",
|
|
376
|
+
"detection_source": "package.json present (no tsconfig.json)"}
|
|
377
|
+
if rank <= len(_MANIFEST_RULES):
|
|
378
|
+
_name, language, source = _MANIFEST_RULES[rank - 1]
|
|
379
|
+
return {"language": language, "confidence": "high", "detection_source": source}
|
|
380
|
+
if rank == 1 + len(_MANIFEST_RULES):
|
|
381
|
+
# build.gradle (Groovy DSL): build.gradle.kts, earlier in the table,
|
|
382
|
+
# already decided when both sit in the same folder depth.
|
|
383
|
+
if context["kotlin"]:
|
|
384
|
+
return {"language": "kotlin", "confidence": "medium",
|
|
385
|
+
"detection_source": "build.gradle (Groovy) + src/main/kotlin/ present"}
|
|
386
|
+
return {"language": "java", "confidence": "medium",
|
|
387
|
+
"detection_source": "build.gradle (Groovy) present without src/main/kotlin/ \u2014 defaulting to java"}
|
|
388
|
+
_suffix, language, source = _SUFFIX_RULES[rank - 2 - len(_MANIFEST_RULES)]
|
|
389
|
+
return {"language": language, "confidence": "high", "detection_source": source}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _ranked_manifests(tree: list[str]) -> dict[str, Any]:
|
|
393
|
+
"""The tree's manifests, ranked for the winner walk (see the module
|
|
394
|
+
docstring): `deciding` and `other` list (depth, rule rank, path, result)
|
|
395
|
+
tuples in rank order, `deciding` the manifests that may decide and
|
|
396
|
+
`other` the non-core ones that may not."""
|
|
397
|
+
root = _tree_root(tree)
|
|
398
|
+
core: list[tuple[int, int, str]] = []
|
|
399
|
+
rest: list[tuple[int, int, str]] = []
|
|
400
|
+
tsconfig = {True: False, False: False} # a tsconfig.json in a core / non-core folder
|
|
401
|
+
for path in tree:
|
|
402
|
+
parts = _parts(path)
|
|
403
|
+
if not parts:
|
|
404
|
+
continue
|
|
405
|
+
folders = parts[len(root):-1]
|
|
406
|
+
is_core = _is_core(folders)
|
|
407
|
+
if parts[-1] == "tsconfig.json":
|
|
408
|
+
tsconfig[is_core] = True
|
|
409
|
+
rank = _rule_of(parts[-1])
|
|
410
|
+
if rank is not None:
|
|
411
|
+
(core if is_core else rest).append((len(folders), rank, path))
|
|
412
|
+
# A source path such as src/main/kotlin/com/example/App.kt is the
|
|
413
|
+
# project's own whatever its folder names: the Kotlin check reads them all.
|
|
414
|
+
kotlin = _has_path_segment(tree, "src/main/kotlin/")
|
|
415
|
+
whole = {"tsconfig": tsconfig[True] or tsconfig[False], "kotlin": kotlin}
|
|
416
|
+
context = {"tsconfig": tsconfig[True], "kotlin": kotlin} if core else whole
|
|
417
|
+
deciding, other = (core, rest) if core else (rest, [])
|
|
418
|
+
return {
|
|
419
|
+
"deciding": [(d, r, p, _rule_result(r, context)) for d, r, p in sorted(deciding)],
|
|
420
|
+
"other": [(d, r, p, _rule_result(r, whole)) for d, r, p in sorted(other)],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
214
424
|
def _detected_languages(payload: dict[str, Any], winner: dict[str, Any]) -> list[str]:
|
|
215
425
|
"""Accumulate every manifest-level match into an ordered, deduplicated list.
|
|
216
426
|
|
|
217
|
-
The walk
|
|
218
|
-
winner["language"] == detected_languages[0] whenever the
|
|
219
|
-
fired
|
|
427
|
+
The walk follows the winner walk's ranking exactly (folder depth, then
|
|
428
|
+
rule order), so winner["language"] == detected_languages[0] whenever the
|
|
429
|
+
manifest table fired; the non-core manifests follow, then, when no
|
|
430
|
+
manifest sits at the root, the tree's source language. Two decisive
|
|
431
|
+
short-circuits diverge from a full walk on purpose:
|
|
220
432
|
|
|
221
433
|
* A workspace_signal override (rule 0) is authoritative — the workspace
|
|
222
434
|
root language wins over any nested package.json, so this returns a
|
|
223
435
|
single-element [winner_language] and never surfaces a spurious
|
|
224
436
|
multi-language gate for what workspace detection already resolved.
|
|
225
|
-
* When no manifest matched (rule
|
|
437
|
+
* When no manifest matched (rule 11 fired), the list carries the
|
|
226
438
|
extension-frequency best guess as a single element, or [] when the
|
|
227
439
|
guess was "unknown".
|
|
228
440
|
"""
|
|
@@ -232,35 +444,21 @@ def _detected_languages(payload: dict[str, Any], winner: dict[str, Any]) -> list
|
|
|
232
444
|
if isinstance(workspace_signal, str) and workspace_signal in _WORKSPACE_SIGNAL_LANGUAGE:
|
|
233
445
|
return [winner["language"]]
|
|
234
446
|
|
|
447
|
+
ranked = _ranked_manifests(tree)
|
|
235
448
|
langs: list[str] = []
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
langs.append(lang)
|
|
240
|
-
|
|
241
|
-
# Rule 1 — package.json (tsconfig.json disambiguation)
|
|
242
|
-
if _has_basename(tree, "package.json"):
|
|
243
|
-
add("typescript" if _has_basename(tree, "tsconfig.json") else "javascript")
|
|
244
|
-
|
|
245
|
-
# Rules 2-6 / 8 / 10 — single-basename manifests (same order as _MANIFEST_RULES)
|
|
246
|
-
for basename, language, _source in _MANIFEST_RULES:
|
|
247
|
-
if _has_basename(tree, basename):
|
|
248
|
-
add(language)
|
|
249
|
-
|
|
250
|
-
# Rule 7b — build.gradle (Groovy) Java/Kotlin disambiguation
|
|
251
|
-
if _has_basename(tree, "build.gradle"):
|
|
252
|
-
add("kotlin" if _has_path_segment(tree, "src/main/kotlin/") else "java")
|
|
253
|
-
|
|
254
|
-
# Rules 8-9 — suffix-based (csproj, sln)
|
|
255
|
-
for suffix, language, _source in _SUFFIX_RULES:
|
|
256
|
-
if _has_suffix(tree, suffix):
|
|
257
|
-
add(language)
|
|
449
|
+
for _depth, _rank, _path, result in ranked["deciding"] + ranked["other"]:
|
|
450
|
+
if result["language"] not in langs:
|
|
451
|
+
langs.append(result["language"])
|
|
258
452
|
|
|
259
453
|
if langs:
|
|
454
|
+
no_root_manifest = ranked["deciding"][0][0] > 0
|
|
455
|
+
source = winner["source_language"]
|
|
456
|
+
if no_root_manifest and source != "unknown" and source not in langs:
|
|
457
|
+
langs.append(source)
|
|
260
458
|
return langs
|
|
261
459
|
|
|
262
460
|
# No manifest matched — the winner came from the extension-frequency
|
|
263
|
-
# fallback (rule
|
|
461
|
+
# fallback (rule 11). Carry its best guess, or [] when it was "unknown".
|
|
264
462
|
fallback_language = winner["language"]
|
|
265
463
|
return [] if fallback_language == "unknown" else [fallback_language]
|
|
266
464
|
|
|
@@ -269,11 +467,13 @@ def detect(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
269
467
|
"""Apply the documented rule walk and annotate the full manifest match set.
|
|
270
468
|
|
|
271
469
|
Returns the single-winner envelope (language/confidence/detection_source/
|
|
272
|
-
fallback_to_extension_frequency)
|
|
273
|
-
|
|
274
|
-
|
|
470
|
+
fallback_to_extension_frequency) plus source_language, the
|
|
471
|
+
extension-frequency answer, and detected_languages[] (the ordered,
|
|
472
|
+
deduplicated set of every manifest-level match), so a caller can both
|
|
473
|
+
auto-pick and gate multi-language disambiguation off one JSON shape.
|
|
275
474
|
"""
|
|
276
475
|
winner = _winner(payload)
|
|
476
|
+
winner["source_language"] = _frequency_fallback(payload["tree"])["language"]
|
|
277
477
|
winner["detected_languages"] = _detected_languages(payload, winner)
|
|
278
478
|
return winner
|
|
279
479
|
|
|
@@ -298,66 +498,216 @@ def _winner(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
298
498
|
"fallback_to_extension_frequency": False,
|
|
299
499
|
}
|
|
300
500
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
501
|
+
# Rules 1-10: the manifest table, over the manifests nearest the
|
|
502
|
+
# tree's root (the non-core folders left out unless nothing else holds
|
|
503
|
+
# a manifest): the first ranked manifest decides.
|
|
504
|
+
deciding = _ranked_manifests(tree)["deciding"]
|
|
505
|
+
if deciding:
|
|
506
|
+
depth, _rank, path, result = deciding[0]
|
|
507
|
+
if depth > 0:
|
|
508
|
+
# No manifest at the root: one below it is a guess about the
|
|
509
|
+
# whole tree, never a high-confidence answer.
|
|
510
|
+
result = {
|
|
511
|
+
"language": result["language"],
|
|
512
|
+
"confidence": "medium",
|
|
513
|
+
"detection_source": f"{result['detection_source']} at {path} (no manifest at the tree's root)",
|
|
309
514
|
}
|
|
310
|
-
return {
|
|
311
|
-
"language": "javascript",
|
|
312
|
-
"confidence": "high",
|
|
313
|
-
"detection_source": "package.json present (no tsconfig.json)",
|
|
314
|
-
"fallback_to_extension_frequency": False,
|
|
315
|
-
}
|
|
515
|
+
return {**result, "fallback_to_extension_frequency": False}
|
|
316
516
|
|
|
317
|
-
#
|
|
318
|
-
|
|
319
|
-
# tree-aware Java/Kotlin disambiguation handled in Rule 7b below.
|
|
320
|
-
# build.gradle.kts IS in the table (returns kotlin high) and fires
|
|
321
|
-
# here before the Groovy variant is considered.
|
|
322
|
-
for basename, language, source in _MANIFEST_RULES:
|
|
323
|
-
if _has_basename(tree, basename):
|
|
324
|
-
return {
|
|
325
|
-
"language": language,
|
|
326
|
-
"confidence": "high",
|
|
327
|
-
"detection_source": source,
|
|
328
|
-
"fallback_to_extension_frequency": False,
|
|
329
|
-
}
|
|
517
|
+
# Rule 11: extension-frequency fallback
|
|
518
|
+
return _frequency_fallback(tree)
|
|
330
519
|
|
|
331
|
-
# Rule 7b — build.gradle (Groovy DSL): check src/main/kotlin/ to disambiguate.
|
|
332
|
-
# This rule sits *after* the basename loop because build.gradle.kts is
|
|
333
|
-
# already covered by the loop and we don't want it to pre-empt that match.
|
|
334
|
-
if _has_basename(tree, "build.gradle"):
|
|
335
|
-
if _has_path_segment(tree, "src/main/kotlin/"):
|
|
336
|
-
return {
|
|
337
|
-
"language": "kotlin",
|
|
338
|
-
"confidence": "medium",
|
|
339
|
-
"detection_source": "build.gradle (Groovy) + src/main/kotlin/ present",
|
|
340
|
-
"fallback_to_extension_frequency": False,
|
|
341
|
-
}
|
|
342
|
-
return {
|
|
343
|
-
"language": "java",
|
|
344
|
-
"confidence": "medium",
|
|
345
|
-
"detection_source": "build.gradle (Groovy) present without src/main/kotlin/ — defaulting to java",
|
|
346
|
-
"fallback_to_extension_frequency": False,
|
|
347
|
-
}
|
|
348
520
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
521
|
+
# --------------------------------------------------------------------------
|
|
522
|
+
# scope-patterns: a brief's include and exclude patterns
|
|
523
|
+
# --------------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
# Per language: the extensions an include lists, the conventional source
|
|
526
|
+
# folders in the order they are tried (None: the whole tree, as Go keeps its
|
|
527
|
+
# packages in the module's own folders; a leading `**/` matches the folder at
|
|
528
|
+
# any depth) and the exclude globs, where `{ext}` stands for each included
|
|
529
|
+
# extension. `unknown` has no extension: it matches every file.
|
|
530
|
+
_SCOPE_RULES: dict[str, tuple[tuple[str, ...], tuple[str, ...] | None, tuple[str, ...]]] = {
|
|
531
|
+
"typescript": ((".ts", ".tsx"), ("src", "lib"), ("**/*.test{ext}", "**/*.spec{ext}", "**/node_modules/**")),
|
|
532
|
+
"javascript": ((".js", ".jsx", ".mjs", ".cjs"), ("src", "lib"),
|
|
533
|
+
("**/*.test{ext}", "**/*.spec{ext}", "**/node_modules/**")),
|
|
534
|
+
"python": ((".py",), ("src",), ("**/*_test.py", "**/test_*.py", "**/tests/**")),
|
|
535
|
+
"rust": ((".rs",), ("src",), ("**/tests/**", "**/benches/**")),
|
|
536
|
+
"go": ((".go",), None, ("**/*_test.go", "**/vendor/**")),
|
|
537
|
+
"java": ((".java",), ("src/main/java", "**/src/main/java"), ("**/src/test/**",)),
|
|
538
|
+
"kotlin": ((".kt",), ("src/main/kotlin", "**/src/main/kotlin"), ("**/src/test/**",)),
|
|
539
|
+
"swift": ((".swift",), ("Sources",), ("**/Tests/**",)),
|
|
540
|
+
"csharp": ((".cs",), ("src",), ("**/bin/**", "**/obj/**", "**/*Tests/**")),
|
|
541
|
+
"ruby": ((".rb",), ("lib",), ("**/spec/**", "**/test/**")),
|
|
542
|
+
"php": ((".php",), ("src", "lib"), ("**/tests/**", "**/vendor/**")),
|
|
543
|
+
"unknown": ((), (), ("**/test/**", "**/tests/**")),
|
|
544
|
+
}
|
|
358
545
|
|
|
359
|
-
|
|
360
|
-
|
|
546
|
+
|
|
547
|
+
def _depth_below(parts: list[str], folder: str) -> int:
|
|
548
|
+
"""How many of a file's folder segments reach down to `folder` (whose
|
|
549
|
+
leading `**/` matches it at any depth), or 0 when the file is not below
|
|
550
|
+
it. Only those segments are judged core: a Java package path below
|
|
551
|
+
src/main/java/ (com/example/, org/.../samples/) is the project's own."""
|
|
552
|
+
segs = folder.split("/")
|
|
553
|
+
folders = parts[:-1]
|
|
554
|
+
if segs[0] != "**":
|
|
555
|
+
return len(segs) if folders[:len(segs)] == segs and _is_core(segs) else 0
|
|
556
|
+
tail = segs[1:]
|
|
557
|
+
for i in range(len(folders) - len(tail) + 1):
|
|
558
|
+
if folders[i:i + len(tail)] == tail and _is_core(folders[:i + len(tail)]):
|
|
559
|
+
return i + len(tail)
|
|
560
|
+
return 0
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# Folders that hold a copy of someone else's code: a manifest there is no
|
|
564
|
+
# package of the repository's own (scope_patterns' merged-monorepo check).
|
|
565
|
+
_VENDORED_SEGMENTS = frozenset({"vendor", "vendored", "third_party", "third-party", "thirdparty"})
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _leaves_out_a_package(paths: list[list[str]], files: list[list[str]], folders: list[str]) -> bool:
|
|
569
|
+
"""Whether a package below the root (a core folder, not a vendored copy,
|
|
570
|
+
that holds a manifest) holds files of the language that none of
|
|
571
|
+
`folders` covers, in a folder of its own that is core: a monorepo merged
|
|
572
|
+
into one skill, whose root src/ or lib/ is one package among its members
|
|
573
|
+
(aws-sdk-js-v3's lib/ beside clients/ and packages/)."""
|
|
574
|
+
packages = {tuple(parts[:-1]) for parts in paths
|
|
575
|
+
if len(parts) > 1 and _rule_of(parts[-1]) is not None and _is_core(parts[:-1])
|
|
576
|
+
and not any(seg.lower() in _VENDORED_SEGMENTS for seg in parts[:-1])}
|
|
577
|
+
for parts in files:
|
|
578
|
+
if any(_depth_below(parts, folder) for folder in folders):
|
|
579
|
+
continue
|
|
580
|
+
below = parts[:-1]
|
|
581
|
+
if any(tuple(below[:len(pkg)]) == pkg and _is_core(below[:len(pkg) + 1]) for pkg in packages):
|
|
582
|
+
return True
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def scope_patterns(tree: list[str], language: str) -> dict[str, Any]:
|
|
587
|
+
"""A brief's include and exclude patterns for `language` from the file
|
|
588
|
+
list alone (see the module docstring)."""
|
|
589
|
+
exts, conventional, excludes = _SCOPE_RULES[language]
|
|
590
|
+
root = _tree_root(tree)
|
|
591
|
+
prefix = "".join(f"{seg}/" for seg in root)
|
|
592
|
+
paths = [parts for parts in (_parts(p)[len(root):] for p in tree) if parts]
|
|
593
|
+
files = [parts for parts in paths if not exts or _extension(parts[-1]) in exts]
|
|
594
|
+
sources = [parts for parts in files if _is_core(parts[:-1])]
|
|
595
|
+
folders: list[str] = []
|
|
596
|
+
merged = False # a source folder held files but left a package's out
|
|
597
|
+
for folder in conventional or ():
|
|
598
|
+
if merged and not folder.startswith("**/"):
|
|
599
|
+
continue
|
|
600
|
+
if any(_depth_below(parts, folder) for parts in files):
|
|
601
|
+
if not _leaves_out_a_package(paths, files, [folder]):
|
|
602
|
+
folders = [folder]
|
|
603
|
+
break
|
|
604
|
+
merged = True
|
|
605
|
+
if not folders and not merged and language == "python":
|
|
606
|
+
packages = sorted({parts[0] for parts in sources if len(parts) == 2 and parts[1] == "__init__.py"})
|
|
607
|
+
if packages and not _leaves_out_a_package(paths, files, packages):
|
|
608
|
+
folders = packages
|
|
609
|
+
left_out: list[str] = []
|
|
610
|
+
if not folders:
|
|
611
|
+
left_out = sorted({parts[0] for parts in files if len(parts) > 1 and not _is_core(parts[:1])})
|
|
612
|
+
chosen = ([parts for parts in files if any(_depth_below(parts, f) for f in folders)] if folders
|
|
613
|
+
else sources)
|
|
614
|
+
held = [ext for ext in exts if any(_extension(parts[-1]) == ext for parts in chosen)] or list(exts)
|
|
615
|
+
names = [f"*{ext}" for ext in held] or ["*"]
|
|
616
|
+
bases = [f"{prefix}{folder}/" for folder in folders] or [prefix]
|
|
617
|
+
exclude: list[str] = []
|
|
618
|
+
for glob in excludes:
|
|
619
|
+
for ext in held if "{ext}" in glob else [""]:
|
|
620
|
+
if glob.replace("{ext}", ext) not in exclude:
|
|
621
|
+
exclude.append(glob.replace("{ext}", ext))
|
|
622
|
+
return {
|
|
623
|
+
"language": language,
|
|
624
|
+
"source_folders": [base.rstrip("/") or "." for base in bases],
|
|
625
|
+
"include": [f"{base}**/{name}" for base in bases for name in names],
|
|
626
|
+
"exclude": exclude + [f"{prefix}{folder}/**" for folder in left_out],
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _scope_patterns_main(argv: list[str]) -> int:
|
|
631
|
+
parser = argparse.ArgumentParser(
|
|
632
|
+
prog="skf-detect-language.py scope-patterns",
|
|
633
|
+
description="Print a brief's scope.include and scope.exclude for one language from a file list.",
|
|
634
|
+
)
|
|
635
|
+
parser.add_argument("--tree-file", required=True,
|
|
636
|
+
help="the repository's file list, as the detector's --tree-file reads it; - reads stdin")
|
|
637
|
+
parser.add_argument("--language", required=True, choices=sorted(_SCOPE_RULES),
|
|
638
|
+
help="the language the skill documents: a value the detector's `language` takes")
|
|
639
|
+
args = parser.parse_args(argv)
|
|
640
|
+
try:
|
|
641
|
+
tree = read_tree_file(args.tree_file)[0]
|
|
642
|
+
except TreeListingError as e:
|
|
643
|
+
_die(str(e))
|
|
644
|
+
json.dump(scope_patterns(tree, args.language), sys.stdout, ensure_ascii=False)
|
|
645
|
+
sys.stdout.write("\n")
|
|
646
|
+
return 0
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# --------------------------------------------------------------------------
|
|
650
|
+
# --tree-file listings
|
|
651
|
+
# --------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class TreeListingError(ValueError):
|
|
655
|
+
"""A --tree-file listing that cannot be read, holds no path or reports a failure."""
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def parse_tree_listing(text: str, source: str) -> tuple[list[str], bool]:
|
|
659
|
+
"""(the file paths, whether the listing was cut short) of a --tree-file
|
|
660
|
+
listing: JSON, or one path per line, after a byte order mark (Windows
|
|
661
|
+
PowerShell writes one into a pipe). Only a JSON listing can say it was
|
|
662
|
+
cut short (a GitHub tree's `truncated`). `source` says where the
|
|
663
|
+
listing came from in an error. Raises TreeListingError, also for a
|
|
664
|
+
listing that holds no path (`[]`, or a tree with no file)."""
|
|
665
|
+
stripped = text.lstrip("").strip()
|
|
666
|
+
if not stripped:
|
|
667
|
+
raise TreeListingError(f"the tree listing {source} is empty; the command that wrote it may have failed")
|
|
668
|
+
truncated = False
|
|
669
|
+
if not JSON_LISTING_RE.match(stripped):
|
|
670
|
+
paths = []
|
|
671
|
+
for line in stripped.splitlines():
|
|
672
|
+
path = line.strip().removeprefix("./")
|
|
673
|
+
if path:
|
|
674
|
+
paths.append(path)
|
|
675
|
+
else:
|
|
676
|
+
try:
|
|
677
|
+
data = json.loads(stripped)
|
|
678
|
+
except json.JSONDecodeError as e:
|
|
679
|
+
raise TreeListingError(f"the tree listing {source} is not valid JSON: {e}") from e
|
|
680
|
+
entries = data
|
|
681
|
+
if isinstance(data, dict):
|
|
682
|
+
status = data.get("status")
|
|
683
|
+
if status is not None and status != "ok":
|
|
684
|
+
raise TreeListingError(
|
|
685
|
+
f"the tree listing {source} reports a failure ({status}): {data.get('message') or 'no message'}")
|
|
686
|
+
entries, truncated = data.get("tree"), data.get("truncated") is True
|
|
687
|
+
if not isinstance(entries, list):
|
|
688
|
+
raise TreeListingError(f"the tree listing {source} holds no list of paths (a `tree` list, or a JSON list)")
|
|
689
|
+
paths = []
|
|
690
|
+
for entry in entries:
|
|
691
|
+
if isinstance(entry, str):
|
|
692
|
+
paths.append(entry)
|
|
693
|
+
elif isinstance(entry, dict) and isinstance(entry.get("path"), str) and entry.get("type", "blob") == "blob":
|
|
694
|
+
paths.append(entry["path"])
|
|
695
|
+
if not paths:
|
|
696
|
+
raise TreeListingError(f"the tree listing {source} holds no file path; the command that wrote it may have failed")
|
|
697
|
+
return paths, truncated
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def read_tree_file(value: str) -> tuple[list[str], bool]:
|
|
701
|
+
"""parse_tree_listing() of the file `value` names, or of stdin for `-`
|
|
702
|
+
(a caller reconfigures stdin to UTF-8 first). Raises TreeListingError."""
|
|
703
|
+
if value == "-":
|
|
704
|
+
return parse_tree_listing(sys.stdin.read(), "on stdin")
|
|
705
|
+
try:
|
|
706
|
+
with open(value, encoding="utf-8-sig") as fh:
|
|
707
|
+
text = fh.read()
|
|
708
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
709
|
+
raise TreeListingError(f"cannot read --tree-file {value}: {getattr(e, 'strerror', None) or e}") from e
|
|
710
|
+
return parse_tree_listing(text, f"in {value}")
|
|
361
711
|
|
|
362
712
|
|
|
363
713
|
def _parse_argv(argv: list[str]) -> dict:
|
|
@@ -365,8 +715,35 @@ def _parse_argv(argv: list[str]) -> dict:
|
|
|
365
715
|
description="Detect primary language from a flat repo file tree by walking the documented rule table.",
|
|
366
716
|
)
|
|
367
717
|
parser.add_argument("--json", help="JSON payload (alternative to stdin)")
|
|
718
|
+
parser.add_argument(
|
|
719
|
+
"--tree-file",
|
|
720
|
+
help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin. "
|
|
721
|
+
"With --tree-file <file>, stdin is not read: pass the other keys with --workspace-signal or --json",
|
|
722
|
+
)
|
|
723
|
+
parser.add_argument(
|
|
724
|
+
"--workspace-signal",
|
|
725
|
+
help="the payload's workspace_signal: the manifest_kind skf-detect-workspaces.py returns",
|
|
726
|
+
)
|
|
368
727
|
args = parser.parse_args(argv)
|
|
369
|
-
|
|
728
|
+
if args.tree_file is not None:
|
|
729
|
+
# The listing is the tree; stdin is never read for the payload here.
|
|
730
|
+
payload = _load_payload(args.json) if args.json is not None else {}
|
|
731
|
+
if "tree" in payload:
|
|
732
|
+
_die("pass the tree once: in the payload or with --tree-file, not both")
|
|
733
|
+
try:
|
|
734
|
+
payload["tree"] = read_tree_file(args.tree_file)[0]
|
|
735
|
+
except TreeListingError as e:
|
|
736
|
+
_die(str(e))
|
|
737
|
+
else:
|
|
738
|
+
payload = _load_payload(args.json if args.json is not None else sys.stdin.read())
|
|
739
|
+
if args.workspace_signal is not None:
|
|
740
|
+
if "workspace_signal" in payload:
|
|
741
|
+
_die("pass workspace_signal once: in the payload or with --workspace-signal, not both")
|
|
742
|
+
payload["workspace_signal"] = args.workspace_signal
|
|
743
|
+
return payload
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _load_payload(raw: str) -> dict:
|
|
370
747
|
if not raw or not raw.strip():
|
|
371
748
|
_die("empty input (expected JSON payload on stdin or via --json)")
|
|
372
749
|
try:
|
|
@@ -399,6 +776,8 @@ def _force_utf8(*streams) -> None:
|
|
|
399
776
|
|
|
400
777
|
def main(argv: list[str]) -> int:
|
|
401
778
|
_force_utf8(sys.stdin, sys.stdout)
|
|
779
|
+
if argv[:1] == ["scope-patterns"]:
|
|
780
|
+
return _scope_patterns_main(argv[1:])
|
|
402
781
|
payload = _parse_argv(argv)
|
|
403
782
|
result = detect(payload)
|
|
404
783
|
json.dump(result, sys.stdout, ensure_ascii=False)
|