bmad-module-skill-forge 2.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +143 -18
- package/docs/verifying-a-skill.md +87 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +3 -3
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +31 -59
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +119 -70
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +122 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +209 -264
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +164 -91
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +380 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +253 -223
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +254 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -17,9 +17,11 @@ the layout looks like a monorepo.
|
|
|
17
17
|
ecosystem table this script implements.
|
|
18
18
|
|
|
19
19
|
2. **skf-analyze-source / scan-project.md §2** — Finds the same set of
|
|
20
|
-
manifest files in the broader project-scan pass
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
manifest files in the broader project-scan pass: its `candidates[]`
|
|
21
|
+
are the candidate boundaries, its `services[]` lists the Docker,
|
|
22
|
+
compose and serverless files, whose folders are candidate boundaries
|
|
23
|
+
too, and each manifest's `version` is the version a brief records
|
|
24
|
+
(step-auto-scope and generate-briefs read it).
|
|
23
25
|
|
|
24
26
|
Pulling this into a deterministic script gives both stages identical
|
|
25
27
|
manifest-set semantics (no LLM drift on what counts as a manifest), a stable
|
|
@@ -27,63 +29,173 @@ JSON envelope for the LLM to consume, and a single place to evolve the
|
|
|
27
29
|
ecosystem matrix as new languages land.
|
|
28
30
|
|
|
29
31
|
Subcommand:
|
|
30
|
-
scan <root> [--ecosystems=auto]
|
|
31
|
-
Walk `<root>` (depth
|
|
32
|
-
recognised manifests, parse each
|
|
33
|
-
and emit:
|
|
32
|
+
scan <root> [--ecosystems=auto] [--include-dev] [--workspaces-file <file>]
|
|
33
|
+
Walk `<root>` (every depth, pruning the excluded and hidden
|
|
34
|
+
directories) for recognised manifests, parse each one and emit:
|
|
34
35
|
{
|
|
35
36
|
"manifests": [
|
|
36
37
|
{
|
|
37
38
|
"path": "<rel-from-root, forward-slash>",
|
|
38
39
|
"ecosystem": "<name>",
|
|
39
|
-
"
|
|
40
|
+
"name": "<the package's own name>" | null,
|
|
41
|
+
"private": true | false | null,
|
|
42
|
+
"version": "<the package's own version>" | null,
|
|
43
|
+
"version_dynamic": true | false,
|
|
44
|
+
"description": "<the package's own description>" | null,
|
|
45
|
+
"deps": [{"name": "...", "version": "...|null"}],
|
|
46
|
+
"internal_deps": ["<name of another scanned manifest>", ...]
|
|
40
47
|
},
|
|
41
48
|
...
|
|
42
49
|
],
|
|
43
|
-
"total_unique": N,
|
|
44
|
-
"monorepo": <bool>,
|
|
45
|
-
"
|
|
50
|
+
"total_unique": N, // unique runtime dep names across all manifests
|
|
51
|
+
"monorepo": <bool>, // >1 manifest of same ecosystem at non-overlapping depths
|
|
52
|
+
"folders": [ // each folder holding a manifest ("." for <root>), sorted
|
|
53
|
+
{"path": "apps/web", "names": ["web"]}, // the package names its manifests give
|
|
54
|
+
...
|
|
55
|
+
],
|
|
56
|
+
"services": ["deploy/docker-compose.yml", ...], // each service definition, sorted
|
|
57
|
+
"candidates": [ // the candidate boundaries, sorted by path
|
|
58
|
+
{"path": "apps/web", "names": ["web"], "manifests": ["apps/web/package.json"],
|
|
59
|
+
"services": ["apps/web/Dockerfile"], "own_source_files": N,
|
|
60
|
+
"workspace_member": true | false | null},
|
|
61
|
+
...
|
|
62
|
+
],
|
|
63
|
+
"umbrella_candidates": [
|
|
64
|
+
{"path": "...", "name": "...", "ecosystem": "...",
|
|
65
|
+
"internal_dep_count": N, "member_count": M},
|
|
66
|
+
...
|
|
67
|
+
],
|
|
68
|
+
"searched_filenames": ["package.json", ...], // every manifest name looked for
|
|
69
|
+
"warnings": ["..."] // optional: only present if any warning was emitted
|
|
46
70
|
}
|
|
47
71
|
Default `--ecosystems=auto` detects all supported ecosystems. The flag
|
|
48
|
-
is currently a placeholder for future filtering
|
|
72
|
+
is currently a placeholder for future filtering: `auto` is the only
|
|
49
73
|
accepted value today, but its presence keeps the CLI shape stable.
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
`--include-dev` also lists each manifest's development dependencies in
|
|
76
|
+
its `deps[]`, each tagged `"scope": "dev"` (a dependency with no
|
|
77
|
+
`scope` is a runtime one; a name the manifest already lists as runtime
|
|
78
|
+
is not repeated), and adds `total_unique_dev`: the unique names that
|
|
79
|
+
appear only as dev dependencies. Without the flag the output holds
|
|
80
|
+
runtime dependencies only, as before.
|
|
81
|
+
|
|
82
|
+
`--workspaces-file` reads the JSON skf-detect-workspaces.py printed
|
|
83
|
+
for the same root (its `--snapshot` output too): each candidate's
|
|
84
|
+
`workspace_member` says whether its folder is one of that JSON's
|
|
85
|
+
`workspaces[]` paths. Without it `workspace_member` is null.
|
|
86
|
+
|
|
87
|
+
Manifest fields:
|
|
88
|
+
name the name the manifest gives its own package: package.json,
|
|
89
|
+
composer.json and pyproject.toml ([project] or
|
|
90
|
+
[tool.poetry]) `name`, setup.py and setup.cfg `name`, Cargo
|
|
91
|
+
[package] `name`, the go.mod `module` path, pom.xml
|
|
92
|
+
`groupId:artifactId` (the group inherited from <parent> when
|
|
93
|
+
absent), Package.swift `Package(name:)`; null for
|
|
94
|
+
requirements.txt, Pipfile, Gemfile and Gradle build scripts,
|
|
95
|
+
which name no package.
|
|
96
|
+
private true when the manifest says the package is not published:
|
|
97
|
+
package.json `"private": true`, Cargo `publish = false` (or
|
|
98
|
+
`[]`), the `Private :: Do Not Upload` classifier or Poetry
|
|
99
|
+
`package-mode = false`, pom.xml `maven.deploy.skip`; false
|
|
100
|
+
when the manifest could set such a flag and does not; null
|
|
101
|
+
when it cannot tell (go.mod, composer.json, Package.swift,
|
|
102
|
+
the formats that name no package, a Cargo virtual workspace
|
|
103
|
+
or a Cargo `publish` inherited from the workspace).
|
|
104
|
+
version the version the manifest gives its own package, as it is
|
|
105
|
+
written (no semver check): package.json and composer.json
|
|
106
|
+
`version`, pyproject.toml `[project]` (or `[tool.poetry]`)
|
|
107
|
+
`version`, setup.py and setup.cfg `version` when it is a
|
|
108
|
+
literal, Cargo `[package] version` (a `version.workspace =
|
|
109
|
+
true` read from the `[workspace.package] version` of the
|
|
110
|
+
nearest Cargo.toml above it that has one), pom.xml
|
|
111
|
+
`<version>` (the parent's when absent), a Gradle build
|
|
112
|
+
script's `version = '...'`; null when it gives none (go.mod,
|
|
113
|
+
Package.swift and the formats that name no package).
|
|
114
|
+
version_dynamic true when the manifest says the version is computed at
|
|
115
|
+
build time, so it is not in the manifest: pyproject.toml
|
|
116
|
+
`dynamic = ["version"]`, a setup.cfg `attr:` or `file:`
|
|
117
|
+
version, a setup.py `version=` that is not a literal.
|
|
118
|
+
description the one-line description the manifest gives its package:
|
|
119
|
+
package.json and composer.json `description`,
|
|
120
|
+
pyproject.toml `[project]` (or `[tool.poetry]`)
|
|
121
|
+
`description`, setup.py and setup.cfg `description` when
|
|
122
|
+
it is a literal, Cargo `[package] description` (null for
|
|
123
|
+
one inherited from the workspace), pom.xml `<description>`,
|
|
124
|
+
a Gradle build script's `description = '...'`; else null.
|
|
125
|
+
internal_deps the runtime dependencies that name another scanned manifest
|
|
126
|
+
of the same ecosystem (the workspace members this one uses),
|
|
127
|
+
as those members spell their names, sorted. Names compare
|
|
128
|
+
as the ecosystem does (PEP 503 for Python, `-` and `_` alike
|
|
129
|
+
for Rust, case-insensitive for Composer and Swift), and a
|
|
130
|
+
pom.xml `${project.groupId}` resolves to its own group.
|
|
131
|
+
|
|
132
|
+
services lists the service definitions the walk finds, by path from the
|
|
133
|
+
root: a Docker, compose or serverless file (is_service_definition, which
|
|
134
|
+
skf-disqualify-candidates.py's `service_definition` signal reads too).
|
|
135
|
+
|
|
136
|
+
candidates lists the folders a project scan proposes as boundaries (Candidate
|
|
137
|
+
boundaries in skf-analyze-source's unit-detection-heuristics.md names this
|
|
138
|
+
list), each folder of `folders[]` and each folder that holds a `services[]`
|
|
139
|
+
file, less those a workspace only gathers: a folder is left out when it
|
|
140
|
+
holds no source file of its own (`own_source_files`, the source files under
|
|
141
|
+
it outside every other such folder below it) and another such folder lies
|
|
142
|
+
below it. So a workspace root such as a private package.json that lists
|
|
143
|
+
`workspaces`, or a Cargo virtual workspace, with no code of its own is left
|
|
144
|
+
out, and so is `.` unless it holds code of its own or nothing else is
|
|
145
|
+
proposed. A source file is one skf-detect-language.py's is_source_file()
|
|
146
|
+
counts (the sibling in this folder, which skf-detect-workspaces.py's snapshot
|
|
147
|
+
reads too). Each record gives the folder's `names` (as `folders[]`), the
|
|
148
|
+
`manifests` and `services` files directly in it, `own_source_files` and
|
|
149
|
+
`workspace_member` (see --workspaces-file).
|
|
150
|
+
|
|
151
|
+
umbrella_candidates lists the published manifests (named, `private` not
|
|
152
|
+
true) whose internal_deps cover at least 2 of the other published members of
|
|
153
|
+
their ecosystem, and at least half of them: a root or facade package that
|
|
154
|
+
depends on the members it would re-export. A private package (an example or
|
|
155
|
+
an app) is never a candidate, since a facade is the public surface. Sorted by
|
|
156
|
+
internal_dep_count (highest first), then path. It is a signal for the
|
|
157
|
+
cohesion judgment, not a verdict.
|
|
158
|
+
|
|
159
|
+
Supported ecosystems (manifest filename, then ecosystem): the
|
|
160
|
+
`MANIFEST_ECOSYSTEMS` table below. `references/manifest-patterns.md` in
|
|
161
|
+
skf-create-stack-skill documents it, and test/test-skf-scan-manifests.py
|
|
162
|
+
fails when the two drift.
|
|
66
163
|
|
|
67
164
|
Parsing approach: JSON manifests via stdlib `json`, TOML via stdlib
|
|
68
165
|
`tomllib` (3.11+); text manifests (requirements.txt, Pipfile, setup.py,
|
|
69
166
|
setup.cfg, go.mod, Gemfile, pom.xml, build.gradle*, Package.swift) via
|
|
70
|
-
ad-hoc regex extraction.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
167
|
+
ad-hoc regex extraction. `deps[]` holds production dependencies by default,
|
|
168
|
+
so the consumer-facing list stays focused on runtime libraries. The
|
|
169
|
+
development sections read with `--include-dev`: package.json
|
|
170
|
+
`devDependencies`; pyproject.toml `[dependency-groups]`, the
|
|
171
|
+
`[project.optional-dependencies]` extras named dev, develop, development,
|
|
172
|
+
test, tests, testing, lint, docs or doc, `[tool.poetry.dev-dependencies]`,
|
|
173
|
+
every `[tool.poetry.group.<name>.dependencies]`,
|
|
174
|
+
`[tool.pdm.dev-dependencies]` and `[tool.uv] dev-dependencies`; setup.py and
|
|
175
|
+
setup.cfg `tests_require` and those same extras; Pipfile `[dev-packages]`;
|
|
176
|
+
Cargo `[dev-dependencies]`; pom.xml `<scope>test</scope>`; Gradle `test*`
|
|
177
|
+
and `androidTest*` configurations; Gemfile gems inside a `group
|
|
178
|
+
:development` or `:test` block; composer.json `require-dev`. requirements.txt,
|
|
179
|
+
go.mod and Package.swift have no development section. Unparseable manifests
|
|
180
|
+
emit a top-level `warnings[]` entry and the manifest still appears in
|
|
181
|
+
`manifests[]` with whatever deps were salvageable (possibly empty).
|
|
75
182
|
|
|
76
183
|
Exit codes:
|
|
77
|
-
0
|
|
78
|
-
1
|
|
184
|
+
0 success (including: zero manifests found, which emits an empty result)
|
|
185
|
+
1 user error (bad root path, root not a directory, a --workspaces-file
|
|
186
|
+
that cannot be read as JSON), or skf-detect-language.py missing beside
|
|
187
|
+
this script
|
|
79
188
|
|
|
80
189
|
CLI example:
|
|
81
190
|
uv run skf-scan-manifests.py scan /path/to/repo
|
|
191
|
+
uv run skf-scan-manifests.py scan /path/to/repo --include-dev
|
|
192
|
+
uv run skf-scan-manifests.py scan /path/to/repo --workspaces-file snapshot.json
|
|
82
193
|
"""
|
|
83
194
|
|
|
84
195
|
from __future__ import annotations
|
|
85
196
|
|
|
86
197
|
import argparse
|
|
198
|
+
import importlib.util
|
|
87
199
|
import json
|
|
88
200
|
import re
|
|
89
201
|
import sys
|
|
@@ -101,6 +213,9 @@ from typing import Iterable
|
|
|
101
213
|
# (we test each name in order, but every match is recorded). Lowercase compare
|
|
102
214
|
# is NOT used — manifest filenames are case-sensitive on POSIX, and Windows
|
|
103
215
|
# CI normalises case during the FS walk anyway.
|
|
216
|
+
# The same order is `searched_filenames`. The Supported Ecosystems table in
|
|
217
|
+
# skf-create-stack-skill's references/manifest-patterns.md mirrors this dict
|
|
218
|
+
# (test/test-skf-scan-manifests.py keeps the two equal).
|
|
104
219
|
MANIFEST_ECOSYSTEMS: dict[str, str] = {
|
|
105
220
|
"package.json": "npm",
|
|
106
221
|
"pyproject.toml": "python",
|
|
@@ -119,7 +234,8 @@ MANIFEST_ECOSYSTEMS: dict[str, str] = {
|
|
|
119
234
|
}
|
|
120
235
|
|
|
121
236
|
# Directories the scan must never descend into. Mirrors
|
|
122
|
-
# references/manifest-patterns.md "Scan Exclusion Patterns"
|
|
237
|
+
# references/manifest-patterns.md "Scan Exclusion Patterns"
|
|
238
|
+
# (test/test-skf-scan-manifests.py keeps the two equal).
|
|
123
239
|
EXCLUDED_DIRS: frozenset[str] = frozenset(
|
|
124
240
|
{
|
|
125
241
|
"node_modules",
|
|
@@ -156,13 +272,56 @@ def _is_excluded_dir(name: str) -> bool:
|
|
|
156
272
|
return False
|
|
157
273
|
|
|
158
274
|
|
|
275
|
+
# A service definition: a Docker, compose or serverless file, by its
|
|
276
|
+
# lower-cased name, and a `Dockerfile.<x>` or `<x>.dockerfile` variant.
|
|
277
|
+
SERVICE_DEFINITION_NAMES: frozenset[str] = frozenset({
|
|
278
|
+
"dockerfile", "containerfile", "docker-compose.yml", "docker-compose.yaml",
|
|
279
|
+
"compose.yml", "compose.yaml", "serverless.yml", "serverless.yaml",
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def is_service_definition(name: str) -> bool:
|
|
284
|
+
"""True for the file name of a service definition (SERVICE_DEFINITION_NAMES)."""
|
|
285
|
+
lname = name.lower()
|
|
286
|
+
return (
|
|
287
|
+
lname in SERVICE_DEFINITION_NAMES
|
|
288
|
+
or lname.startswith("dockerfile.")
|
|
289
|
+
or lname.endswith(".dockerfile")
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
_DETECT_LANGUAGE = None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _is_source_file(name: str) -> bool:
|
|
297
|
+
"""skf-detect-language.py's is_source_file(), the sibling beside this
|
|
298
|
+
script, loaded once. Raises ImportError when it cannot be loaded."""
|
|
299
|
+
global _DETECT_LANGUAGE
|
|
300
|
+
if _DETECT_LANGUAGE is None:
|
|
301
|
+
path = Path(__file__).resolve().parent / "skf-detect-language.py"
|
|
302
|
+
spec = importlib.util.spec_from_file_location("skf_detect_language", path)
|
|
303
|
+
if spec is None or spec.loader is None or not path.is_file():
|
|
304
|
+
raise ImportError(f"cannot load {path.name} beside {Path(__file__).name}")
|
|
305
|
+
module = importlib.util.module_from_spec(spec)
|
|
306
|
+
spec.loader.exec_module(module)
|
|
307
|
+
_DETECT_LANGUAGE = module
|
|
308
|
+
return _DETECT_LANGUAGE.is_source_file(name)
|
|
309
|
+
|
|
310
|
+
|
|
159
311
|
def find_manifests(root: Path) -> list[Path]:
|
|
160
312
|
"""Walk `root` returning every recognised manifest file.
|
|
161
313
|
|
|
162
314
|
Excludes `EXCLUDED_DIRS` and all hidden directories. Returns absolute
|
|
163
315
|
paths in stable sorted order so caller output is reproducible.
|
|
164
316
|
"""
|
|
317
|
+
return _walk(root)[0]
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _walk(root: Path, sources: list[str] | None = None) -> tuple[list[Path], list[Path]]:
|
|
321
|
+
"""(manifests, service definitions) under `root`, as find_manifests
|
|
322
|
+
walks; each source file's path from `root` joins `sources` when given."""
|
|
165
323
|
found: list[Path] = []
|
|
324
|
+
services: list[Path] = []
|
|
166
325
|
# Iterative BFS so we can prune at the directory level.
|
|
167
326
|
stack: list[Path] = [root]
|
|
168
327
|
while stack:
|
|
@@ -179,10 +338,15 @@ def find_manifests(root: Path) -> list[Path]:
|
|
|
179
338
|
stack.append(entry)
|
|
180
339
|
elif entry.is_file() and entry.name in MANIFEST_ECOSYSTEMS:
|
|
181
340
|
found.append(entry)
|
|
341
|
+
elif entry.is_file() and is_service_definition(entry.name):
|
|
342
|
+
services.append(entry)
|
|
343
|
+
elif sources is not None and entry.is_file() and _is_source_file(entry.name):
|
|
344
|
+
sources.append(entry.relative_to(root).as_posix())
|
|
182
345
|
except OSError:
|
|
183
346
|
continue
|
|
184
347
|
found.sort()
|
|
185
|
-
|
|
348
|
+
services.sort()
|
|
349
|
+
return found, services
|
|
186
350
|
|
|
187
351
|
|
|
188
352
|
# --------------------------------------------------------------------------
|
|
@@ -583,23 +747,672 @@ PARSERS = {
|
|
|
583
747
|
}
|
|
584
748
|
|
|
585
749
|
|
|
750
|
+
# --------------------------------------------------------------------------
|
|
751
|
+
# Development dependencies (--include-dev)
|
|
752
|
+
# --------------------------------------------------------------------------
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
# Optional-dependency extras read as development dependencies.
|
|
756
|
+
DEV_EXTRAS: frozenset[str] = frozenset(
|
|
757
|
+
{"dev", "develop", "development", "test", "tests", "testing", "lint", "docs", "doc"}
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _table_deps(
|
|
762
|
+
table: object, *, skip_python: bool = False, star_as_none: bool = False
|
|
763
|
+
) -> list[Dep]:
|
|
764
|
+
"""Deps from a TOML or JSON table of name -> version string or spec table.
|
|
765
|
+
|
|
766
|
+
`skip_python` drops Poetry's `python` constraint; `star_as_none` reads a
|
|
767
|
+
`*` version as no version, as Pipfile does.
|
|
768
|
+
"""
|
|
769
|
+
if not isinstance(table, dict):
|
|
770
|
+
return []
|
|
771
|
+
deps: list[Dep] = []
|
|
772
|
+
for name, spec in table.items():
|
|
773
|
+
if not isinstance(name, str) or (skip_python and name.lower() == "python"):
|
|
774
|
+
continue
|
|
775
|
+
if isinstance(spec, dict):
|
|
776
|
+
spec = spec.get("version")
|
|
777
|
+
version = spec if isinstance(spec, str) else None
|
|
778
|
+
if star_as_none and version == "*":
|
|
779
|
+
version = None
|
|
780
|
+
deps.append({"name": name, "version": version})
|
|
781
|
+
return deps
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _requirement_list(entries: object) -> list[Dep]:
|
|
785
|
+
"""Deps from a list of PEP 508 strings (tables such as include-group skipped)."""
|
|
786
|
+
if not isinstance(entries, list):
|
|
787
|
+
return []
|
|
788
|
+
deps: list[Dep] = []
|
|
789
|
+
for entry in entries:
|
|
790
|
+
if isinstance(entry, str):
|
|
791
|
+
parsed = _parse_pep508_or_pip(entry)
|
|
792
|
+
if parsed:
|
|
793
|
+
deps.append(parsed)
|
|
794
|
+
return deps
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def dev_package_json(text: str) -> list[Dep]:
|
|
798
|
+
try:
|
|
799
|
+
data = json.loads(text)
|
|
800
|
+
except json.JSONDecodeError:
|
|
801
|
+
return []
|
|
802
|
+
return _table_deps(data.get("devDependencies")) if isinstance(data, dict) else []
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def dev_pyproject_toml(text: str) -> list[Dep]:
|
|
806
|
+
try:
|
|
807
|
+
data = tomllib.loads(text)
|
|
808
|
+
except tomllib.TOMLDecodeError:
|
|
809
|
+
return []
|
|
810
|
+
deps: list[Dep] = []
|
|
811
|
+
groups = data.get("dependency-groups")
|
|
812
|
+
if isinstance(groups, dict):
|
|
813
|
+
for entries in groups.values():
|
|
814
|
+
deps.extend(_requirement_list(entries))
|
|
815
|
+
project = data.get("project")
|
|
816
|
+
extras = project.get("optional-dependencies") if isinstance(project, dict) else None
|
|
817
|
+
if isinstance(extras, dict):
|
|
818
|
+
for extra, entries in extras.items():
|
|
819
|
+
if isinstance(extra, str) and extra.lower() in DEV_EXTRAS:
|
|
820
|
+
deps.extend(_requirement_list(entries))
|
|
821
|
+
tool = data.get("tool") if isinstance(data.get("tool"), dict) else {}
|
|
822
|
+
poetry = tool.get("poetry")
|
|
823
|
+
if isinstance(poetry, dict):
|
|
824
|
+
deps.extend(_table_deps(poetry.get("dev-dependencies"), skip_python=True))
|
|
825
|
+
poetry_groups = poetry.get("group")
|
|
826
|
+
if isinstance(poetry_groups, dict):
|
|
827
|
+
for group in poetry_groups.values():
|
|
828
|
+
if isinstance(group, dict):
|
|
829
|
+
deps.extend(_table_deps(group.get("dependencies"), skip_python=True))
|
|
830
|
+
pdm = tool.get("pdm")
|
|
831
|
+
if isinstance(pdm, dict) and isinstance(pdm.get("dev-dependencies"), dict):
|
|
832
|
+
for entries in pdm["dev-dependencies"].values():
|
|
833
|
+
deps.extend(_requirement_list(entries))
|
|
834
|
+
uv = tool.get("uv")
|
|
835
|
+
if isinstance(uv, dict):
|
|
836
|
+
deps.extend(_requirement_list(uv.get("dev-dependencies")))
|
|
837
|
+
return deps
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def dev_setup_py(text: str) -> list[Dep]:
|
|
841
|
+
deps: list[Dep] = []
|
|
842
|
+
m = re.search(r"tests_require\s*=\s*\[([^\]]*)\]", text, re.DOTALL)
|
|
843
|
+
if m:
|
|
844
|
+
for match in re.finditer(r"""(['"])([^'"]+)\1""", m.group(1)):
|
|
845
|
+
parsed = _parse_pep508_or_pip(match.group(2).strip())
|
|
846
|
+
if parsed:
|
|
847
|
+
deps.append(parsed)
|
|
848
|
+
extras = re.search(r"extras_require\s*=\s*\{(.*?)\}", text, re.DOTALL)
|
|
849
|
+
if extras:
|
|
850
|
+
for group in re.finditer(
|
|
851
|
+
r"""(['"])([\w.-]+)\1\s*:\s*\[([^\]]*)\]""", extras.group(1)
|
|
852
|
+
):
|
|
853
|
+
if group.group(2).lower() not in DEV_EXTRAS:
|
|
854
|
+
continue
|
|
855
|
+
for match in re.finditer(r"""(['"])([^'"]+)\1""", group.group(3)):
|
|
856
|
+
parsed = _parse_pep508_or_pip(match.group(2).strip())
|
|
857
|
+
if parsed:
|
|
858
|
+
deps.append(parsed)
|
|
859
|
+
return deps
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _cfg_section(text: str, name: str) -> str:
|
|
863
|
+
sec = re.search(
|
|
864
|
+
rf"^\[{re.escape(name)}\][ \t]*$(.*?)(?=^\[|\Z)", text, re.MULTILINE | re.DOTALL
|
|
865
|
+
)
|
|
866
|
+
return sec.group(1) if sec else ""
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _cfg_values(value: str) -> list[Dep]:
|
|
870
|
+
deps: list[Dep] = []
|
|
871
|
+
for line in value.splitlines():
|
|
872
|
+
parsed = _parse_pep508_or_pip(line.strip())
|
|
873
|
+
if parsed:
|
|
874
|
+
deps.append(parsed)
|
|
875
|
+
return deps
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
# `key = value` in a setup.cfg section, with its indented continuation lines.
|
|
879
|
+
_CFG_OPTION = re.compile(
|
|
880
|
+
r"^(?P<key>[\w.-]+)[ \t]*=[ \t]*(?P<value>.*(?:\n[ \t]+\S.*)*)", re.MULTILINE
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def dev_setup_cfg(text: str) -> list[Dep]:
|
|
885
|
+
deps: list[Dep] = []
|
|
886
|
+
for m in _CFG_OPTION.finditer(_cfg_section(text, "options")):
|
|
887
|
+
if m.group("key") == "tests_require":
|
|
888
|
+
deps.extend(_cfg_values(m.group("value")))
|
|
889
|
+
for m in _CFG_OPTION.finditer(_cfg_section(text, "options.extras_require")):
|
|
890
|
+
if m.group("key").lower() in DEV_EXTRAS:
|
|
891
|
+
deps.extend(_cfg_values(m.group("value")))
|
|
892
|
+
return deps
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def dev_pipfile(text: str) -> list[Dep]:
|
|
896
|
+
try:
|
|
897
|
+
data = tomllib.loads(text)
|
|
898
|
+
except tomllib.TOMLDecodeError:
|
|
899
|
+
return []
|
|
900
|
+
return _table_deps(data.get("dev-packages"), star_as_none=True)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def dev_cargo_toml(text: str) -> list[Dep]:
|
|
904
|
+
try:
|
|
905
|
+
data = tomllib.loads(text)
|
|
906
|
+
except tomllib.TOMLDecodeError:
|
|
907
|
+
return []
|
|
908
|
+
return _table_deps(data.get("dev-dependencies"))
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
def dev_pom_xml(text: str) -> list[Dep]:
|
|
912
|
+
deps: list[Dep] = []
|
|
913
|
+
for block in re.finditer(r"<dependency>(.*?)</dependency>", text, re.DOTALL):
|
|
914
|
+
body = block.group(1)
|
|
915
|
+
scope_m = re.search(r"<scope>\s*(.*?)\s*</scope>", body)
|
|
916
|
+
if not scope_m or scope_m.group(1).strip().lower() != "test":
|
|
917
|
+
continue
|
|
918
|
+
gid = re.search(r"<groupId>\s*(.*?)\s*</groupId>", body)
|
|
919
|
+
aid = re.search(r"<artifactId>\s*(.*?)\s*</artifactId>", body)
|
|
920
|
+
ver = re.search(r"<version>\s*(.*?)\s*</version>", body)
|
|
921
|
+
if gid and aid:
|
|
922
|
+
deps.append(
|
|
923
|
+
{
|
|
924
|
+
"name": f"{gid.group(1).strip()}:{aid.group(1).strip()}",
|
|
925
|
+
"version": ver.group(1).strip() if ver else None,
|
|
926
|
+
}
|
|
927
|
+
)
|
|
928
|
+
return deps
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
# Whitespace or `(` must follow the configuration, so the `"test"` inside
|
|
932
|
+
# `testImplementation(kotlin("test"))` never starts a match.
|
|
933
|
+
_GRADLE_DEV_DEP = re.compile(
|
|
934
|
+
r"""\b(?:test|androidTest)[A-Za-z]*
|
|
935
|
+
(?:\s+|\s*\(\s*)
|
|
936
|
+
(?:['"])
|
|
937
|
+
(?P<coord>[^'"\n]+)
|
|
938
|
+
(?:['"])
|
|
939
|
+
""",
|
|
940
|
+
re.VERBOSE,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def dev_gradle(text: str) -> list[Dep]:
|
|
945
|
+
deps: list[Dep] = []
|
|
946
|
+
for m in _GRADLE_DEV_DEP.finditer(text):
|
|
947
|
+
parts = m.group("coord").split(":")
|
|
948
|
+
if len(parts) >= 2:
|
|
949
|
+
deps.append(
|
|
950
|
+
{"name": f"{parts[0]}:{parts[1]}", "version": parts[2] if len(parts) >= 3 else None}
|
|
951
|
+
)
|
|
952
|
+
return deps
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def dev_gemfile(text: str) -> list[Dep]:
|
|
956
|
+
"""Gems inside `group :development` / `group :test` blocks."""
|
|
957
|
+
deps: list[Dep] = []
|
|
958
|
+
depth = 0
|
|
959
|
+
for raw_line in text.splitlines():
|
|
960
|
+
line = raw_line.split("#", 1)[0]
|
|
961
|
+
if re.search(r"group\s+:(development|test)\b", line):
|
|
962
|
+
depth += 1
|
|
963
|
+
continue
|
|
964
|
+
if depth > 0:
|
|
965
|
+
if re.search(r"\bend\b", line):
|
|
966
|
+
depth -= 1
|
|
967
|
+
continue
|
|
968
|
+
m = _GEMFILE_LINE.match(line)
|
|
969
|
+
if m:
|
|
970
|
+
deps.append({"name": m.group("name"), "version": m.group("version")})
|
|
971
|
+
return deps
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def dev_composer_json(text: str) -> list[Dep]:
|
|
975
|
+
try:
|
|
976
|
+
data = json.loads(text)
|
|
977
|
+
except json.JSONDecodeError:
|
|
978
|
+
return []
|
|
979
|
+
if not isinstance(data, dict):
|
|
980
|
+
return []
|
|
981
|
+
return [
|
|
982
|
+
d
|
|
983
|
+
for d in _table_deps(data.get("require-dev"))
|
|
984
|
+
if d["name"].lower() != "php" and not d["name"].startswith("ext-")
|
|
985
|
+
]
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
DEV_PARSERS = {
|
|
989
|
+
"package.json": dev_package_json,
|
|
990
|
+
"pyproject.toml": dev_pyproject_toml,
|
|
991
|
+
"setup.py": dev_setup_py,
|
|
992
|
+
"setup.cfg": dev_setup_cfg,
|
|
993
|
+
"Pipfile": dev_pipfile,
|
|
994
|
+
"Cargo.toml": dev_cargo_toml,
|
|
995
|
+
"pom.xml": dev_pom_xml,
|
|
996
|
+
"build.gradle": dev_gradle,
|
|
997
|
+
"build.gradle.kts": dev_gradle,
|
|
998
|
+
"Gemfile": dev_gemfile,
|
|
999
|
+
"composer.json": dev_composer_json,
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
# --------------------------------------------------------------------------
|
|
1004
|
+
# Package identity: the manifest's own name and publish flag
|
|
1005
|
+
# --------------------------------------------------------------------------
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
PRIVATE_CLASSIFIER = "Private :: Do Not Upload"
|
|
1009
|
+
|
|
1010
|
+
Identity = tuple # (name: str | None, private: bool | None)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def identity_package_json(text: str) -> Identity:
|
|
1014
|
+
try:
|
|
1015
|
+
data = json.loads(text)
|
|
1016
|
+
except json.JSONDecodeError:
|
|
1017
|
+
return None, None
|
|
1018
|
+
if not isinstance(data, dict):
|
|
1019
|
+
return None, None
|
|
1020
|
+
name = data.get("name") if isinstance(data.get("name"), str) and data.get("name") else None
|
|
1021
|
+
return name, data.get("private") is True
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def identity_pyproject_toml(text: str) -> Identity:
|
|
1025
|
+
try:
|
|
1026
|
+
data = tomllib.loads(text)
|
|
1027
|
+
except tomllib.TOMLDecodeError:
|
|
1028
|
+
return None, None
|
|
1029
|
+
project = data.get("project") if isinstance(data.get("project"), dict) else {}
|
|
1030
|
+
tool = data.get("tool") if isinstance(data.get("tool"), dict) else {}
|
|
1031
|
+
poetry = tool.get("poetry") if isinstance(tool.get("poetry"), dict) else {}
|
|
1032
|
+
name = project.get("name") or poetry.get("name")
|
|
1033
|
+
name = name if isinstance(name, str) and name else None
|
|
1034
|
+
if not project and not poetry:
|
|
1035
|
+
return None, None
|
|
1036
|
+
classifiers = [
|
|
1037
|
+
c
|
|
1038
|
+
for source in (project.get("classifiers"), poetry.get("classifiers"))
|
|
1039
|
+
if isinstance(source, list)
|
|
1040
|
+
for c in source
|
|
1041
|
+
]
|
|
1042
|
+
private = PRIVATE_CLASSIFIER in classifiers or poetry.get("package-mode") is False
|
|
1043
|
+
return name, private
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def identity_setup_py(text: str) -> Identity:
|
|
1047
|
+
call = re.search(r"\bsetup\s*\((.*)", text, re.DOTALL)
|
|
1048
|
+
m = re.search(r"""\bname\s*=\s*(['"])([^'"]+)\1""", call.group(1)) if call else None
|
|
1049
|
+
if not m:
|
|
1050
|
+
return None, None
|
|
1051
|
+
return m.group(2), PRIVATE_CLASSIFIER in text
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def identity_setup_cfg(text: str) -> Identity:
|
|
1055
|
+
metadata = _cfg_section(text, "metadata")
|
|
1056
|
+
m = re.search(r"^name[ \t]*=[ \t]*(\S+)", metadata, re.MULTILINE)
|
|
1057
|
+
if not m:
|
|
1058
|
+
return None, None
|
|
1059
|
+
return m.group(1), PRIVATE_CLASSIFIER in metadata
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def identity_cargo_toml(text: str) -> Identity:
|
|
1063
|
+
try:
|
|
1064
|
+
data = tomllib.loads(text)
|
|
1065
|
+
except tomllib.TOMLDecodeError:
|
|
1066
|
+
return None, None
|
|
1067
|
+
package = data.get("package")
|
|
1068
|
+
if not isinstance(package, dict):
|
|
1069
|
+
return None, None # a virtual workspace manifest names no package
|
|
1070
|
+
name = package.get("name") if isinstance(package.get("name"), str) else None
|
|
1071
|
+
publish = package.get("publish")
|
|
1072
|
+
if isinstance(publish, dict):
|
|
1073
|
+
return name, None # `publish.workspace = true`: set by the workspace root
|
|
1074
|
+
return name, publish is False or publish == []
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def identity_go_mod(text: str) -> Identity:
|
|
1078
|
+
m = re.search(r"""^\s*module\s+["`]?([^\s"`]+)""", text, re.MULTILINE)
|
|
1079
|
+
return (m.group(1) if m else None), None
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
# Sections of a pom.xml whose <groupId>/<artifactId> are not the project's own.
|
|
1083
|
+
_POM_FOREIGN_SECTIONS = (
|
|
1084
|
+
"parent",
|
|
1085
|
+
"dependencies",
|
|
1086
|
+
"dependencyManagement",
|
|
1087
|
+
"build",
|
|
1088
|
+
"profiles",
|
|
1089
|
+
"reporting",
|
|
1090
|
+
"modules",
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def identity_pom_xml(text: str) -> Identity:
|
|
1095
|
+
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
|
1096
|
+
parent = re.search(r"<parent>(.*?)</parent>", text, re.DOTALL)
|
|
1097
|
+
own = text
|
|
1098
|
+
for tag in _POM_FOREIGN_SECTIONS:
|
|
1099
|
+
own = re.sub(rf"<{tag}>.*?</{tag}>", "", own, flags=re.DOTALL)
|
|
1100
|
+
aid = re.search(r"<artifactId>\s*(.*?)\s*</artifactId>", own)
|
|
1101
|
+
if not aid:
|
|
1102
|
+
return None, None
|
|
1103
|
+
gid = re.search(r"<groupId>\s*(.*?)\s*</groupId>", own)
|
|
1104
|
+
if not gid and parent:
|
|
1105
|
+
gid = re.search(r"<groupId>\s*(.*?)\s*</groupId>", parent.group(1))
|
|
1106
|
+
name = f"{gid.group(1)}:{aid.group(1)}" if gid else aid.group(1)
|
|
1107
|
+
skip = re.search(r"<maven\.deploy\.skip>\s*true\s*</maven\.deploy\.skip>", own)
|
|
1108
|
+
return name, skip is not None
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def identity_composer_json(text: str) -> Identity:
|
|
1112
|
+
try:
|
|
1113
|
+
data = json.loads(text)
|
|
1114
|
+
except json.JSONDecodeError:
|
|
1115
|
+
return None, None
|
|
1116
|
+
if not isinstance(data, dict):
|
|
1117
|
+
return None, None
|
|
1118
|
+
name = data.get("name")
|
|
1119
|
+
return (name if isinstance(name, str) and name else None), None
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def identity_package_swift(text: str) -> Identity:
|
|
1123
|
+
m = re.search(r'\bPackage\s*\(\s*name\s*:\s*"([^"]+)"', text)
|
|
1124
|
+
return (m.group(1) if m else None), None
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
# --------------------------------------------------------------------------
|
|
1128
|
+
# Package version: the version the manifest gives its own package
|
|
1129
|
+
# --------------------------------------------------------------------------
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
# (version, how): `how` is "dynamic" for a version computed at build time,
|
|
1133
|
+
# "workspace" for a Cargo `version.workspace = true`, else None.
|
|
1134
|
+
Version = tuple
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def _literal(value: object) -> str | None:
|
|
1138
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def version_package_json(text: str) -> Version:
|
|
1142
|
+
data = json.loads(text)
|
|
1143
|
+
return (_literal(data.get("version")) if isinstance(data, dict) else None), None
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def version_pyproject_toml(text: str) -> Version:
|
|
1147
|
+
data = tomllib.loads(text)
|
|
1148
|
+
project = data.get("project") if isinstance(data.get("project"), dict) else {}
|
|
1149
|
+
tool = data.get("tool") if isinstance(data.get("tool"), dict) else {}
|
|
1150
|
+
poetry = tool.get("poetry") if isinstance(tool.get("poetry"), dict) else {}
|
|
1151
|
+
version = _literal(project.get("version")) or _literal(poetry.get("version"))
|
|
1152
|
+
dynamic = isinstance(project.get("dynamic"), list) and "version" in project["dynamic"]
|
|
1153
|
+
return version, ("dynamic" if dynamic and version is None else None)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def version_setup_py(text: str) -> Version:
|
|
1157
|
+
call = re.search(r"\bsetup\s*\((.*)", text, re.DOTALL)
|
|
1158
|
+
if not call:
|
|
1159
|
+
return None, None
|
|
1160
|
+
m = re.search(r"""\bversion\s*=\s*(['"])([^'"]+)\1""", call.group(1))
|
|
1161
|
+
if m:
|
|
1162
|
+
return m.group(2).strip(), None
|
|
1163
|
+
return None, ("dynamic" if re.search(r"\bversion\s*=", call.group(1)) else None)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def version_setup_cfg(text: str) -> Version:
|
|
1167
|
+
m = re.search(r"^version[ \t]*=[ \t]*(\S+)", _cfg_section(text, "metadata"), re.MULTILINE)
|
|
1168
|
+
if not m:
|
|
1169
|
+
return None, None
|
|
1170
|
+
return (None, "dynamic") if m.group(1).startswith(("attr:", "file:")) else (m.group(1), None)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def version_cargo_toml(text: str) -> Version:
|
|
1174
|
+
package = tomllib.loads(text).get("package")
|
|
1175
|
+
if not isinstance(package, dict):
|
|
1176
|
+
return None, None
|
|
1177
|
+
version = package.get("version")
|
|
1178
|
+
if isinstance(version, dict) and version.get("workspace") is True:
|
|
1179
|
+
return None, "workspace"
|
|
1180
|
+
return _literal(version), None
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def cargo_workspace_version(text: str) -> str | None:
|
|
1184
|
+
"""The `[workspace.package] version` a Cargo workspace root gives its members."""
|
|
1185
|
+
workspace = tomllib.loads(text).get("workspace")
|
|
1186
|
+
package = workspace.get("package") if isinstance(workspace, dict) else None
|
|
1187
|
+
return _literal(package.get("version")) if isinstance(package, dict) else None
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def version_pom_xml(text: str) -> Version:
|
|
1191
|
+
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
|
1192
|
+
parent = re.search(r"<parent>(.*?)</parent>", text, re.DOTALL)
|
|
1193
|
+
own = text
|
|
1194
|
+
for tag in _POM_FOREIGN_SECTIONS:
|
|
1195
|
+
own = re.sub(rf"<{tag}>.*?</{tag}>", "", own, flags=re.DOTALL)
|
|
1196
|
+
m = re.search(r"<version>\s*(.*?)\s*</version>", own)
|
|
1197
|
+
if not m and parent:
|
|
1198
|
+
m = re.search(r"<version>\s*(.*?)\s*</version>", parent.group(1))
|
|
1199
|
+
return (m.group(1) or None) if m else None, None
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def version_gradle(text: str) -> Version:
|
|
1203
|
+
m = re.search(r"""^\s*version\s*=\s*['"]([^'"]+)['"]""", text, re.MULTILINE)
|
|
1204
|
+
return (m.group(1) if m else None), None
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
VERSION_READERS = {
|
|
1208
|
+
"package.json": version_package_json,
|
|
1209
|
+
"pyproject.toml": version_pyproject_toml,
|
|
1210
|
+
"setup.py": version_setup_py,
|
|
1211
|
+
"setup.cfg": version_setup_cfg,
|
|
1212
|
+
"Cargo.toml": version_cargo_toml,
|
|
1213
|
+
"pom.xml": version_pom_xml,
|
|
1214
|
+
"build.gradle": version_gradle,
|
|
1215
|
+
"build.gradle.kts": version_gradle,
|
|
1216
|
+
"composer.json": version_package_json,
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
# --------------------------------------------------------------------------
|
|
1221
|
+
# Package description: the one line the manifest gives its own package
|
|
1222
|
+
# --------------------------------------------------------------------------
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def description_package_json(text: str) -> str | None:
|
|
1226
|
+
data = json.loads(text)
|
|
1227
|
+
return _literal(data.get("description")) if isinstance(data, dict) else None
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def description_pyproject_toml(text: str) -> str | None:
|
|
1231
|
+
data = tomllib.loads(text)
|
|
1232
|
+
project = data.get("project") if isinstance(data.get("project"), dict) else {}
|
|
1233
|
+
tool = data.get("tool") if isinstance(data.get("tool"), dict) else {}
|
|
1234
|
+
poetry = tool.get("poetry") if isinstance(tool.get("poetry"), dict) else {}
|
|
1235
|
+
return _literal(project.get("description")) or _literal(poetry.get("description"))
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def description_setup_py(text: str) -> str | None:
|
|
1239
|
+
call = re.search(r"\bsetup\s*\((.*)", text, re.DOTALL)
|
|
1240
|
+
m = re.search(r"""\bdescription\s*=\s*(['"])([^'"]+)\1""", call.group(1)) if call else None
|
|
1241
|
+
return _literal(m.group(2)) if m else None
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
def description_setup_cfg(text: str) -> str | None:
|
|
1245
|
+
m = re.search(r"^description[ \t]*=[ \t]*(.+)$", _cfg_section(text, "metadata"), re.MULTILINE)
|
|
1246
|
+
return _literal(m.group(1)) if m and not m.group(1).startswith(("attr:", "file:")) else None
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def description_cargo_toml(text: str) -> str | None:
|
|
1250
|
+
package = tomllib.loads(text).get("package")
|
|
1251
|
+
return _literal(package.get("description")) if isinstance(package, dict) else None
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
def description_pom_xml(text: str) -> str | None:
|
|
1255
|
+
own = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
|
1256
|
+
for tag in _POM_FOREIGN_SECTIONS:
|
|
1257
|
+
own = re.sub(rf"<{tag}>.*?</{tag}>", "", own, flags=re.DOTALL)
|
|
1258
|
+
m = re.search(r"<description>\s*(.*?)\s*</description>", own, re.DOTALL)
|
|
1259
|
+
return _literal(" ".join(m.group(1).split())) if m else None
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
def description_gradle(text: str) -> str | None:
|
|
1263
|
+
m = re.search(r"""^\s*description\s*=\s*['"]([^'"]+)['"]""", text, re.MULTILINE)
|
|
1264
|
+
return _literal(m.group(1)) if m else None
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
DESCRIPTION_READERS = {
|
|
1268
|
+
"package.json": description_package_json,
|
|
1269
|
+
"pyproject.toml": description_pyproject_toml,
|
|
1270
|
+
"setup.py": description_setup_py,
|
|
1271
|
+
"setup.cfg": description_setup_cfg,
|
|
1272
|
+
"Cargo.toml": description_cargo_toml,
|
|
1273
|
+
"pom.xml": description_pom_xml,
|
|
1274
|
+
"build.gradle": description_gradle,
|
|
1275
|
+
"build.gradle.kts": description_gradle,
|
|
1276
|
+
"composer.json": description_package_json,
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
IDENTITY_READERS = {
|
|
1281
|
+
"package.json": identity_package_json,
|
|
1282
|
+
"pyproject.toml": identity_pyproject_toml,
|
|
1283
|
+
"setup.py": identity_setup_py,
|
|
1284
|
+
"setup.cfg": identity_setup_cfg,
|
|
1285
|
+
"Cargo.toml": identity_cargo_toml,
|
|
1286
|
+
"go.mod": identity_go_mod,
|
|
1287
|
+
"pom.xml": identity_pom_xml,
|
|
1288
|
+
"composer.json": identity_composer_json,
|
|
1289
|
+
"Package.swift": identity_package_swift,
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
|
|
586
1293
|
# --------------------------------------------------------------------------
|
|
587
1294
|
# Aggregation
|
|
588
1295
|
# --------------------------------------------------------------------------
|
|
589
1296
|
|
|
590
1297
|
|
|
591
|
-
def
|
|
592
|
-
"""Read
|
|
1298
|
+
def _describe_manifest(path: Path, include_dev: bool) -> tuple[dict, list[str]]:
|
|
1299
|
+
"""Read one manifest once: its identity, runtime deps and (optionally)
|
|
1300
|
+
dev deps tagged `scope: dev`. Returns (record fields, warnings)."""
|
|
1301
|
+
record: dict = {"name": None, "private": None, "version": None, "version_how": None,
|
|
1302
|
+
"workspace_version": None, "description": None, "deps": []}
|
|
593
1303
|
parser = PARSERS.get(path.name)
|
|
594
1304
|
if parser is None:
|
|
595
|
-
return
|
|
1305
|
+
return record, [f"{path.name}: no parser registered"]
|
|
596
1306
|
text = _safe_read_text(path)
|
|
597
1307
|
if text is None:
|
|
598
|
-
return
|
|
1308
|
+
return record, [f"{path.name}: file unreadable"]
|
|
1309
|
+
try:
|
|
1310
|
+
deps, warnings = parser(text)
|
|
1311
|
+
except Exception as exc: # noqa: BLE001 (best-effort parsing)
|
|
1312
|
+
deps, warnings = [], [f"{path.name}: parser raised {type(exc).__name__}: {exc}"]
|
|
1313
|
+
# identity and dev readers are best-effort too; the runtime parser has
|
|
1314
|
+
# already reported a malformed file
|
|
1315
|
+
reader = IDENTITY_READERS.get(path.name)
|
|
1316
|
+
try:
|
|
1317
|
+
record["name"], record["private"] = reader(text) if reader else (None, None)
|
|
1318
|
+
except Exception: # noqa: BLE001
|
|
1319
|
+
pass
|
|
1320
|
+
version_reader = VERSION_READERS.get(path.name)
|
|
599
1321
|
try:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1322
|
+
if version_reader:
|
|
1323
|
+
record["version"], record["version_how"] = version_reader(text)
|
|
1324
|
+
if path.name == "Cargo.toml":
|
|
1325
|
+
record["workspace_version"] = cargo_workspace_version(text)
|
|
1326
|
+
except Exception: # noqa: BLE001
|
|
1327
|
+
pass
|
|
1328
|
+
description_reader = DESCRIPTION_READERS.get(path.name)
|
|
1329
|
+
try:
|
|
1330
|
+
record["description"] = description_reader(text) if description_reader else None
|
|
1331
|
+
except Exception: # noqa: BLE001
|
|
1332
|
+
pass
|
|
1333
|
+
if include_dev and path.name in DEV_PARSERS:
|
|
1334
|
+
try:
|
|
1335
|
+
dev = DEV_PARSERS[path.name](text)
|
|
1336
|
+
except Exception: # noqa: BLE001
|
|
1337
|
+
dev = []
|
|
1338
|
+
runtime = {d["name"] for d in deps}
|
|
1339
|
+
for dep in dev:
|
|
1340
|
+
if dep["name"] not in runtime:
|
|
1341
|
+
runtime.add(dep["name"])
|
|
1342
|
+
deps.append({**dep, "scope": "dev"})
|
|
1343
|
+
record["deps"] = deps
|
|
1344
|
+
return record, warnings
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _member_key(ecosystem: str, name: str) -> str:
|
|
1348
|
+
"""How an ecosystem compares package names."""
|
|
1349
|
+
if ecosystem == "python":
|
|
1350
|
+
return re.sub(r"[-_.]+", "-", name).lower()
|
|
1351
|
+
if ecosystem == "rust":
|
|
1352
|
+
return name.lower().replace("_", "-")
|
|
1353
|
+
if ecosystem in ("composer", "swift"):
|
|
1354
|
+
return name.lower()
|
|
1355
|
+
return name
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
def _add_internal_deps(manifests: list[dict]) -> None:
|
|
1359
|
+
"""Set each manifest's `internal_deps`: its runtime deps that name another
|
|
1360
|
+
scanned manifest of the same ecosystem, spelled as that member names itself."""
|
|
1361
|
+
members: dict[str, dict[str, str]] = {}
|
|
1362
|
+
for m in manifests:
|
|
1363
|
+
if m["name"]:
|
|
1364
|
+
members.setdefault(m["ecosystem"], {}).setdefault(
|
|
1365
|
+
_member_key(m["ecosystem"], m["name"]), m["name"]
|
|
1366
|
+
)
|
|
1367
|
+
for m in manifests:
|
|
1368
|
+
ecosystem = m["ecosystem"]
|
|
1369
|
+
known = members.get(ecosystem, {})
|
|
1370
|
+
own = _member_key(ecosystem, m["name"]) if m["name"] else None
|
|
1371
|
+
# a pom.xml names sibling modules as ${project.groupId}:<artifact>
|
|
1372
|
+
group = None
|
|
1373
|
+
if ecosystem == "maven" and m["name"] and ":" in m["name"]:
|
|
1374
|
+
group = m["name"].split(":", 1)[0]
|
|
1375
|
+
internal: set[str] = set()
|
|
1376
|
+
for dep in m["deps"]:
|
|
1377
|
+
name = dep.get("name")
|
|
1378
|
+
if not isinstance(name, str) or dep.get("scope") == "dev":
|
|
1379
|
+
continue
|
|
1380
|
+
if group:
|
|
1381
|
+
for token in ("${project.groupId}", "${pom.groupId}", "${groupId}"):
|
|
1382
|
+
name = name.replace(token, group)
|
|
1383
|
+
key = _member_key(ecosystem, name)
|
|
1384
|
+
if key != own and key in known:
|
|
1385
|
+
internal.add(known[key])
|
|
1386
|
+
m["internal_deps"] = sorted(internal)
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def _umbrella_candidates(manifests: list[dict]) -> list[dict]:
|
|
1390
|
+
"""Published manifests whose internal_deps cover at least 2, and at least
|
|
1391
|
+
half, of the other published members of their ecosystem."""
|
|
1392
|
+
published: dict[str, set[str]] = {}
|
|
1393
|
+
for m in manifests:
|
|
1394
|
+
if m["name"] and m["private"] is not True:
|
|
1395
|
+
published.setdefault(m["ecosystem"], set()).add(_member_key(m["ecosystem"], m["name"]))
|
|
1396
|
+
candidates: list[dict] = []
|
|
1397
|
+
for m in manifests:
|
|
1398
|
+
if not m["name"] or m["private"] is True:
|
|
1399
|
+
continue
|
|
1400
|
+
ecosystem = m["ecosystem"]
|
|
1401
|
+
own = _member_key(ecosystem, m["name"])
|
|
1402
|
+
pool = published.get(ecosystem, set()) - {own}
|
|
1403
|
+
covered = {_member_key(ecosystem, n) for n in m["internal_deps"]} & pool
|
|
1404
|
+
if len(covered) >= 2 and 2 * len(covered) >= len(pool):
|
|
1405
|
+
candidates.append(
|
|
1406
|
+
{
|
|
1407
|
+
"path": m["path"],
|
|
1408
|
+
"name": m["name"],
|
|
1409
|
+
"ecosystem": ecosystem,
|
|
1410
|
+
"internal_dep_count": len(covered),
|
|
1411
|
+
"member_count": len(pool),
|
|
1412
|
+
}
|
|
1413
|
+
)
|
|
1414
|
+
candidates.sort(key=lambda c: (-c["internal_dep_count"], c["path"]))
|
|
1415
|
+
return candidates
|
|
603
1416
|
|
|
604
1417
|
|
|
605
1418
|
def _is_monorepo(manifests: list[dict]) -> bool:
|
|
@@ -641,37 +1454,135 @@ def _path_is_ancestor(a: str, b: str) -> bool:
|
|
|
641
1454
|
return b.startswith(a + "/")
|
|
642
1455
|
|
|
643
1456
|
|
|
644
|
-
def
|
|
645
|
-
"""
|
|
646
|
-
|
|
1457
|
+
def _folders(manifests: list[dict]) -> list[dict]:
|
|
1458
|
+
"""Each folder holding a manifest ("." for the root), with the names its manifests give."""
|
|
1459
|
+
by_folder: dict[str, set[str]] = {}
|
|
1460
|
+
for m in manifests:
|
|
1461
|
+
folder = m["path"].rsplit("/", 1)[0] if "/" in m["path"] else "."
|
|
1462
|
+
names = by_folder.setdefault(folder, set())
|
|
1463
|
+
if isinstance(m["name"], str) and m["name"]:
|
|
1464
|
+
names.add(m["name"])
|
|
1465
|
+
return [{"path": folder, "names": sorted(names)} for folder, names in sorted(by_folder.items())]
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def _folder_of(rel: str) -> str:
|
|
1469
|
+
"""The folder of a path relative to the scan root, "." for the root itself."""
|
|
1470
|
+
return rel.rsplit("/", 1)[0] if "/" in rel else "."
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def _candidates(manifests: list[dict], services: list[str], sources: list[str],
|
|
1474
|
+
members: set[str] | None) -> list[dict]:
|
|
1475
|
+
"""The candidate boundaries: see "candidates" in the module docstring.
|
|
1476
|
+
`members` is the workspace paths --workspaces-file gives, else None."""
|
|
1477
|
+
folders: dict[str, dict] = {}
|
|
1478
|
+
|
|
1479
|
+
def folder_entry(path: str) -> dict:
|
|
1480
|
+
return folders.setdefault(
|
|
1481
|
+
_folder_of(path), {"names": set(), "manifests": [], "services": []}
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
for m in manifests:
|
|
1485
|
+
entry = folder_entry(m["path"])
|
|
1486
|
+
entry["manifests"].append(m["path"])
|
|
1487
|
+
if isinstance(m["name"], str) and m["name"]:
|
|
1488
|
+
entry["names"].add(m["name"])
|
|
1489
|
+
for path in services:
|
|
1490
|
+
folder_entry(path)["services"].append(path)
|
|
1491
|
+
own = dict.fromkeys(folders, 0)
|
|
1492
|
+
for path in sources:
|
|
1493
|
+
folder = _folder_of(path)
|
|
1494
|
+
while folder not in folders and folder != ".":
|
|
1495
|
+
folder = _folder_of(folder)
|
|
1496
|
+
if folder in folders:
|
|
1497
|
+
own[folder] += 1
|
|
1498
|
+
out: list[dict] = []
|
|
1499
|
+
for folder in sorted(folders):
|
|
1500
|
+
below = any(
|
|
1501
|
+
other != folder and (folder == "." or other.startswith(folder + "/"))
|
|
1502
|
+
for other in folders
|
|
1503
|
+
)
|
|
1504
|
+
if own[folder] == 0 and below:
|
|
1505
|
+
continue
|
|
1506
|
+
entry = folders[folder]
|
|
1507
|
+
out.append({
|
|
1508
|
+
"path": folder,
|
|
1509
|
+
"names": sorted(entry["names"]),
|
|
1510
|
+
"manifests": entry["manifests"],
|
|
1511
|
+
"services": entry["services"],
|
|
1512
|
+
"own_source_files": own[folder],
|
|
1513
|
+
"workspace_member": None if members is None else folder in members,
|
|
1514
|
+
})
|
|
1515
|
+
return out
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
def scan(root: Path, include_dev: bool = False, workspaces: set[str] | None = None) -> dict:
|
|
1519
|
+
"""Run a full manifest scan rooted at `root`.
|
|
1520
|
+
|
|
1521
|
+
With `include_dev`, each manifest's development dependencies join its
|
|
1522
|
+
`deps[]` tagged `scope: dev`, and `total_unique_dev` is added.
|
|
1523
|
+
`workspaces` is the workspace paths that set each candidate's
|
|
1524
|
+
`workspace_member` (None leaves it null). Raises ImportError when
|
|
1525
|
+
skf-detect-language.py does not sit beside this script.
|
|
1526
|
+
"""
|
|
1527
|
+
sources: list[str] = []
|
|
1528
|
+
paths, services = _walk(root, sources)
|
|
647
1529
|
manifests: list[dict] = []
|
|
648
1530
|
warnings: list[str] = []
|
|
1531
|
+
# the folder of a Cargo workspace root: the version its members inherit
|
|
1532
|
+
workspace_versions: dict[str, str] = {}
|
|
1533
|
+
inheriting: list[dict] = []
|
|
649
1534
|
|
|
650
1535
|
for path in paths:
|
|
651
|
-
|
|
1536
|
+
record, warns = _describe_manifest(path, include_dev)
|
|
652
1537
|
rel = path.relative_to(root).as_posix()
|
|
653
1538
|
for w in warns:
|
|
654
1539
|
warnings.append(f"{rel}: {w}")
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1540
|
+
manifest = {
|
|
1541
|
+
"path": rel,
|
|
1542
|
+
"ecosystem": MANIFEST_ECOSYSTEMS[path.name],
|
|
1543
|
+
"name": record["name"],
|
|
1544
|
+
"private": record["private"],
|
|
1545
|
+
"version": record["version"],
|
|
1546
|
+
"version_dynamic": record["version_how"] == "dynamic",
|
|
1547
|
+
"description": record["description"],
|
|
1548
|
+
"deps": record["deps"],
|
|
1549
|
+
}
|
|
1550
|
+
manifests.append(manifest)
|
|
1551
|
+
if record["workspace_version"]:
|
|
1552
|
+
workspace_versions[_folder_of(rel)] = record["workspace_version"]
|
|
1553
|
+
if record["version_how"] == "workspace":
|
|
1554
|
+
inheriting.append(manifest)
|
|
1555
|
+
for manifest in inheriting:
|
|
1556
|
+
folder = _folder_of(manifest["path"])
|
|
1557
|
+
while True:
|
|
1558
|
+
if folder in workspace_versions:
|
|
1559
|
+
manifest["version"] = workspace_versions[folder]
|
|
1560
|
+
break
|
|
1561
|
+
if folder == ".":
|
|
1562
|
+
break
|
|
1563
|
+
folder = _folder_of(folder)
|
|
1564
|
+
_add_internal_deps(manifests)
|
|
662
1565
|
|
|
663
1566
|
unique_names: set[str] = set()
|
|
1567
|
+
dev_names: set[str] = set()
|
|
664
1568
|
for m in manifests:
|
|
665
1569
|
for dep in m["deps"]:
|
|
666
1570
|
name = dep.get("name")
|
|
667
1571
|
if isinstance(name, str) and name and name != "<unparsable>":
|
|
668
|
-
unique_names.add(name)
|
|
1572
|
+
(dev_names if dep.get("scope") == "dev" else unique_names).add(name)
|
|
669
1573
|
|
|
670
1574
|
result: dict = {
|
|
671
1575
|
"manifests": manifests,
|
|
672
1576
|
"total_unique": len(unique_names),
|
|
673
|
-
"monorepo": _is_monorepo(manifests),
|
|
674
1577
|
}
|
|
1578
|
+
if include_dev:
|
|
1579
|
+
result["total_unique_dev"] = len(dev_names - unique_names)
|
|
1580
|
+
result["monorepo"] = _is_monorepo(manifests)
|
|
1581
|
+
result["folders"] = _folders(manifests)
|
|
1582
|
+
result["services"] = [path.relative_to(root).as_posix() for path in services]
|
|
1583
|
+
result["candidates"] = _candidates(manifests, result["services"], sources, workspaces)
|
|
1584
|
+
result["umbrella_candidates"] = _umbrella_candidates(manifests)
|
|
1585
|
+
result["searched_filenames"] = list(MANIFEST_ECOSYSTEMS)
|
|
675
1586
|
if warnings:
|
|
676
1587
|
result["warnings"] = warnings
|
|
677
1588
|
return result
|
|
@@ -693,11 +1604,30 @@ def _cmd_scan(args: argparse.Namespace) -> int:
|
|
|
693
1604
|
file=sys.stderr,
|
|
694
1605
|
)
|
|
695
1606
|
return 1
|
|
1607
|
+
workspaces = None
|
|
1608
|
+
if args.workspaces_file is not None:
|
|
1609
|
+
try:
|
|
1610
|
+
data = json.loads(Path(args.workspaces_file).read_text(encoding="utf-8-sig"))
|
|
1611
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
1612
|
+
print(
|
|
1613
|
+
f"error: cannot read --workspaces-file {args.workspaces_file}: {exc}",
|
|
1614
|
+
file=sys.stderr,
|
|
1615
|
+
)
|
|
1616
|
+
return 1
|
|
1617
|
+
listed = data.get("workspaces") if isinstance(data, dict) else None
|
|
1618
|
+
workspaces = {
|
|
1619
|
+
w["path"].strip("/").removeprefix("./")
|
|
1620
|
+
for w in (listed if isinstance(listed, list) else [])
|
|
1621
|
+
if isinstance(w, dict) and isinstance(w.get("path"), str)
|
|
1622
|
+
}
|
|
696
1623
|
try:
|
|
697
|
-
result = scan(root)
|
|
1624
|
+
result = scan(root, include_dev=args.include_dev, workspaces=workspaces)
|
|
698
1625
|
except OSError as exc:
|
|
699
1626
|
print(f"error: filesystem error during scan: {exc}", file=sys.stderr)
|
|
700
1627
|
return 1
|
|
1628
|
+
except ImportError as exc:
|
|
1629
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1630
|
+
return 1
|
|
701
1631
|
json.dump(result, sys.stdout, indent=2)
|
|
702
1632
|
sys.stdout.write("\n")
|
|
703
1633
|
return 0
|
|
@@ -723,6 +1653,17 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
723
1653
|
default="auto",
|
|
724
1654
|
help="ecosystem filter (currently only 'auto' is accepted)",
|
|
725
1655
|
)
|
|
1656
|
+
p_scan.add_argument(
|
|
1657
|
+
"--include-dev",
|
|
1658
|
+
action="store_true",
|
|
1659
|
+
help="also list development dependencies, each tagged scope: dev",
|
|
1660
|
+
)
|
|
1661
|
+
p_scan.add_argument(
|
|
1662
|
+
"--workspaces-file",
|
|
1663
|
+
metavar="PATH",
|
|
1664
|
+
help="the JSON skf-detect-workspaces.py printed for the same root: sets each "
|
|
1665
|
+
"candidate's workspace_member",
|
|
1666
|
+
)
|
|
726
1667
|
p_scan.set_defaults(func=_cmd_scan)
|
|
727
1668
|
|
|
728
1669
|
return parser
|