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
|
@@ -4,21 +4,27 @@
|
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Detect Workspaces — pure detector for monorepo / multi-package layouts.
|
|
6
6
|
|
|
7
|
-
Takes a
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
Takes a target repo's file tree plus the contents of a small set of root
|
|
8
|
+
manifests, and returns whether a workspace layout is present, which manifest
|
|
9
|
+
kind drives it, and the list of resolved workspaces. detect() does no file
|
|
10
|
+
I/O: the CLI reads the tree and the manifests from a JSON payload (stdin or
|
|
11
|
+
--json), or from the files --tree-file and --manifest-dir name, so a caller
|
|
12
|
+
(skf-brief-skill step 2 §1b, skf-test-skill's coverage check) can stage them
|
|
13
|
+
instead of typing a tree or a manifest into a shell string. With --snapshot
|
|
14
|
+
it prints the facts the tree gives an analysis instead (see "Tree snapshot"
|
|
15
|
+
below).
|
|
13
16
|
|
|
14
17
|
Detection runs in priority order; the first matching detector wins:
|
|
15
18
|
|
|
16
19
|
1. npm-workspaces package.json has `workspaces: [...]` or `{packages: [...]}`
|
|
17
20
|
2. pnpm-workspaces pnpm-workspace.yaml exists with a `packages:` list
|
|
18
21
|
3. lerna lerna.json exists (`packages` field optional, defaults to `packages/*`)
|
|
19
|
-
4.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
4. rush rush.json lists `projects[]` (JSON with comments; its
|
|
23
|
+
content must be supplied under `manifests`)
|
|
24
|
+
5. nx nx.json at the root, and folders holding a project.json
|
|
25
|
+
6. cargo-workspace Cargo.toml has `[workspace]` with `members = [...]`
|
|
26
|
+
7. python-multi-package multiple pyproject.toml under packages/* or apps/*
|
|
27
|
+
8. generic-folders apps/, packages/, libs/, or code/ each with subdirs
|
|
22
28
|
containing a recognisable manifest
|
|
23
29
|
|
|
24
30
|
Glob members (`packages/*`, `apps/*`, etc.) are resolved against the supplied
|
|
@@ -35,7 +41,9 @@ Input JSON shape (stdin):
|
|
|
35
41
|
"package.json": "<raw text>",
|
|
36
42
|
"Cargo.toml": "<raw text>",
|
|
37
43
|
"pnpm-workspace.yaml": "<raw text>",
|
|
38
|
-
"lerna.json": "<raw text>"
|
|
44
|
+
"lerna.json": "<raw text>",
|
|
45
|
+
"rush.json": "<raw text>",
|
|
46
|
+
"nx.json": "<raw text>"
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
|
@@ -43,17 +51,19 @@ Input JSON shape (stdin):
|
|
|
43
51
|
Per-workspace child manifests (e.g. `packages/foo/package.json`) MUST appear
|
|
44
52
|
here for glob resolution; their contents are optional.
|
|
45
53
|
- `manifests` is a dict keyed by repository-relative path; only root-level
|
|
46
|
-
manifests must be supplied.
|
|
47
|
-
|
|
48
|
-
(the path basename is used
|
|
54
|
+
manifests must be supplied (nx.json may be left out when the tree lists
|
|
55
|
+
it). Per-workspace manifest contents may be included to populate the
|
|
56
|
+
workspace `name` field, but absence is fine (the path basename is used
|
|
57
|
+
as a fallback); for Nx that is each project.json, for Rush the
|
|
58
|
+
`packageName` in rush.json names each project.
|
|
49
59
|
|
|
50
60
|
Output JSON shape (stdout):
|
|
51
61
|
|
|
52
62
|
{
|
|
53
63
|
"is_monorepo": true | false,
|
|
54
64
|
"manifest_kind": "npm-workspaces" | "pnpm-workspaces" | "lerna"
|
|
55
|
-
| "
|
|
56
|
-
| "generic-folders" | null,
|
|
65
|
+
| "rush" | "nx" | "cargo-workspace"
|
|
66
|
+
| "python-multi-package" | "generic-folders" | null,
|
|
57
67
|
"workspaces": [
|
|
58
68
|
{"name": "foo", "path": "packages/foo", "manifest": "packages/foo/package.json"},
|
|
59
69
|
...
|
|
@@ -61,21 +71,117 @@ Output JSON shape (stdout):
|
|
|
61
71
|
"warnings": ["..."]
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
`warnings` also names each root manifest whose content a detector reads
|
|
75
|
+
(CONTENT_MANIFESTS) that the tree lists but `manifests` does not hold: the
|
|
76
|
+
detector that needs it cannot run, so a workspace layout it would find is
|
|
77
|
+
missed.
|
|
78
|
+
|
|
79
|
+
CLI:
|
|
80
|
+
uv run skf-detect-workspaces.py < payload.json
|
|
81
|
+
uv run skf-detect-workspaces.py --json '{"tree": [...], "manifests": {...}}'
|
|
82
|
+
uv run skf-detect-workspaces.py --tree-file <file> --manifest-files
|
|
83
|
+
uv run skf-detect-workspaces.py --tree-file <file> [--manifest-dir <dir>]
|
|
84
|
+
uv run skf-detect-workspaces.py --tree-file <file> [--manifest-dir <dir>] \\
|
|
85
|
+
--snapshot [--root <folder>]
|
|
86
|
+
|
|
87
|
+
--tree-file reads the tree from a file (`-` reads it from stdin) through
|
|
88
|
+
skf-detect-language.py's read_tree_file() (the sibling in this folder, which
|
|
89
|
+
must sit beside this script), whose --tree-file text lists the listings it
|
|
90
|
+
reads; one that holds no path is refused. With --tree-file, stdin is never
|
|
91
|
+
read for the payload: `manifests` comes with --json or --manifest-dir, or is
|
|
92
|
+
empty, and the payload may not carry `tree` as well. When the listing says
|
|
93
|
+
it was cut short (a GitHub tree's `truncated`), `warnings` says so: a
|
|
94
|
+
workspace whose manifest the listing lacks is not found.
|
|
95
|
+
|
|
96
|
+
--manifest-files prints the root manifests the --tree-file listing holds
|
|
97
|
+
whose content a detector reads (CONTENT_MANIFESTS), one repo path per line
|
|
98
|
+
with LF endings on every platform (nothing when there are none), and exits:
|
|
99
|
+
a shell loop fetches exactly those files into the folder --manifest-dir then
|
|
100
|
+
reads.
|
|
101
|
+
|
|
102
|
+
--manifest-dir reads `manifests` from a folder laid out like the repository,
|
|
103
|
+
a local checkout or the folder the --manifest-files paths were fetched into:
|
|
104
|
+
each root manifest of CONTENT_MANIFESTS the tree lists, then the manifest of
|
|
105
|
+
each workspace found, which names it. A file missing there, larger than
|
|
106
|
+
MAX_MANIFEST_BYTES or not UTF-8 was not supplied. Passing `manifests` in the
|
|
107
|
+
payload as well is an error.
|
|
108
|
+
|
|
109
|
+
Tree snapshot (--snapshot)
|
|
110
|
+
--------------------------
|
|
111
|
+
|
|
112
|
+
The facts an analysis of the repository, or of one folder of it (--root,
|
|
113
|
+
such as a workspace path), reads from the tree, counted by the script so no
|
|
114
|
+
caller counts or samples a long listing by hand. It takes the same inputs:
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
"root": "" | "<folder>", the folder described ("" for
|
|
118
|
+
the repository)
|
|
119
|
+
"truncated": bool, the --tree-file listing says it was cut
|
|
120
|
+
short (a GitHub tree's `truncated`); the
|
|
121
|
+
counts are then lower bounds, and a
|
|
122
|
+
workspace may be missed
|
|
123
|
+
"file_count": N, files under the root
|
|
124
|
+
"source_file_count": N, of those, the source files: the
|
|
125
|
+
extensions skf-detect-language.py's
|
|
126
|
+
extension-frequency rule counts
|
|
127
|
+
"dir_count": N, folders under the root
|
|
128
|
+
"top_level_files": [...], names of the files directly in the root
|
|
129
|
+
"top_level_dirs": [...], names of the folders directly in the root
|
|
130
|
+
"manifest_kind": the workspace layout the detectors find at the
|
|
131
|
+
repository root (null without one, and with
|
|
132
|
+
--root)
|
|
133
|
+
"workspaces": [...], its workspaces, as detection lists them
|
|
134
|
+
"module_root": "" | "<folder>", where the module candidates are
|
|
135
|
+
"module_candidates": [{"path": "<folder>", "file_count": N,
|
|
136
|
+
"source_file_count": N}, ...], each folder
|
|
137
|
+
directly in module_root but hidden ones,
|
|
138
|
+
with the files and source files under it
|
|
139
|
+
"registry_candidates": [...], repo paths of the component registry
|
|
140
|
+
files under the root
|
|
141
|
+
"warnings": [...] the detection warnings at the repository
|
|
142
|
+
root
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Modules. Which folders are the modules is the caller's judgment, so the
|
|
146
|
+
snapshot names none: it lists the candidates and what they hold. The module
|
|
147
|
+
root is the root's `src/` folder, else its `lib/` folder, else the root
|
|
148
|
+
itself, followed down while it holds no file and exactly one folder (hidden
|
|
149
|
+
ones and NON_MODULE_DIRS aside) that holds folders of its own (a Python
|
|
150
|
+
`src/<package>/` layout, or Java's `src/main/java/com/acme/`). The modules
|
|
151
|
+
are the workspaces when the detectors find some, else the candidates that
|
|
152
|
+
hold the library's own code (a folder with no source file, or of tests,
|
|
153
|
+
docs, examples, scripts, build tooling or CI, is none); a candidate that
|
|
154
|
+
holds most of the source files is the package itself (pandas/ at the root of
|
|
155
|
+
pandas), and a snapshot with --root <it> lists its own candidates. The count
|
|
156
|
+
of the modules picked is skf-recommend-scope-type.py's `module_count` input.
|
|
157
|
+
|
|
158
|
+
Registry candidates are the files the component-registry rule of
|
|
159
|
+
skf-recommend-scope-type.py reads (registry.ts, components.ts and their .tsx
|
|
160
|
+
forms, at any depth), found by that script's own rule (the sibling in this
|
|
161
|
+
folder, which must sit beside this script).
|
|
162
|
+
|
|
64
163
|
Exit codes:
|
|
65
164
|
|
|
66
165
|
0 success (regardless of is_monorepo value)
|
|
67
166
|
1 payload error (malformed top-level JSON shape)
|
|
68
|
-
2
|
|
167
|
+
2 input error: empty stdin, invalid JSON, a bad or conflicting flag
|
|
168
|
+
(argparse), the tree or the manifests passed twice, a tree listing that
|
|
169
|
+
cannot be read, holds no path or reports a failure, a sibling script that
|
|
170
|
+
cannot be loaded, or a --root under which the tree lists no file
|
|
69
171
|
"""
|
|
70
172
|
|
|
71
173
|
from __future__ import annotations
|
|
72
174
|
|
|
73
175
|
import argparse
|
|
74
176
|
import fnmatch
|
|
177
|
+
import importlib.util
|
|
75
178
|
import json
|
|
179
|
+
import os
|
|
180
|
+
import posixpath
|
|
76
181
|
import re
|
|
77
182
|
import sys
|
|
78
183
|
import tomllib
|
|
184
|
+
from pathlib import Path
|
|
79
185
|
from typing import Optional
|
|
80
186
|
|
|
81
187
|
|
|
@@ -90,6 +196,25 @@ RECOGNISED_MANIFESTS = (
|
|
|
90
196
|
"build.gradle.kts",
|
|
91
197
|
"pom.xml",
|
|
92
198
|
)
|
|
199
|
+
# Root manifests whose content a detector reads (nx.json counts by its place
|
|
200
|
+
# in the tree alone): --manifest-files lists them, --manifest-dir reads them,
|
|
201
|
+
# and a warning names each one the tree lists that was not supplied.
|
|
202
|
+
CONTENT_MANIFESTS = ("package.json", "pnpm-workspace.yaml", "lerna.json", "rush.json", "Cargo.toml")
|
|
203
|
+
MAX_MANIFEST_BYTES = 1024 * 1024
|
|
204
|
+
# Folders a snapshot's module root is never followed down through (names
|
|
205
|
+
# matched in lower case): tests, test helpers and fixtures, docs and sites,
|
|
206
|
+
# examples, playgrounds and benchmarks, build output, vendored dependencies
|
|
207
|
+
# and caches, and non-code resources. They still appear among the module
|
|
208
|
+
# candidates, with what they hold: which folders are modules is the
|
|
209
|
+
# caller's judgment.
|
|
210
|
+
NON_MODULE_DIRS = frozenset({
|
|
211
|
+
"test", "tests", "__tests__", "testing", "spec", "specs", "testdata", "fixture", "fixtures", "e2e",
|
|
212
|
+
"test-utils", "test_utils", "testutils", "__testutils__",
|
|
213
|
+
"doc", "docs", "website", "www", "example", "examples", "demo", "demos", "sample", "samples",
|
|
214
|
+
"playground", "playgrounds", "bench", "benches", "benchmark", "benchmarks", "scripts",
|
|
215
|
+
"build", "dist", "out", "target", "coverage", "node_modules", "vendor", "__pycache__",
|
|
216
|
+
"resources",
|
|
217
|
+
})
|
|
93
218
|
|
|
94
219
|
|
|
95
220
|
# --------------------------------------------------------------------------
|
|
@@ -286,6 +411,117 @@ def detect_lerna(
|
|
|
286
411
|
return _build_workspaces(dirs, tree, manifests)
|
|
287
412
|
|
|
288
413
|
|
|
414
|
+
def _strip_json_comments(content: str) -> str:
|
|
415
|
+
"""JSON with `//` and `/* */` comments and trailing commas (rush.json is
|
|
416
|
+
JSONC) made plain JSON; string contents are left alone."""
|
|
417
|
+
n = len(content)
|
|
418
|
+
|
|
419
|
+
def comment_end(i: int) -> Optional[int]:
|
|
420
|
+
"""The index just past the comment that starts at `i`, or None."""
|
|
421
|
+
if content.startswith("//", i):
|
|
422
|
+
end = content.find("\n", i)
|
|
423
|
+
return n if end < 0 else end
|
|
424
|
+
if content.startswith("/*", i):
|
|
425
|
+
end = content.find("*/", i + 2)
|
|
426
|
+
return n if end < 0 else end + 2
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
def next_token(i: int) -> str:
|
|
430
|
+
"""The first character at or after `i` outside blanks and comments."""
|
|
431
|
+
while i < n:
|
|
432
|
+
end = comment_end(i)
|
|
433
|
+
if end is not None:
|
|
434
|
+
i = end
|
|
435
|
+
elif content[i].isspace():
|
|
436
|
+
i += 1
|
|
437
|
+
else:
|
|
438
|
+
return content[i]
|
|
439
|
+
return ""
|
|
440
|
+
|
|
441
|
+
out: list[str] = []
|
|
442
|
+
i = 0
|
|
443
|
+
while i < n:
|
|
444
|
+
ch = content[i]
|
|
445
|
+
end = comment_end(i)
|
|
446
|
+
if ch == '"':
|
|
447
|
+
j = i + 1
|
|
448
|
+
while j < n and content[j] != '"':
|
|
449
|
+
j += 2 if content[j] == "\\" else 1
|
|
450
|
+
out.append(content[i : j + 1])
|
|
451
|
+
i = j + 1
|
|
452
|
+
elif end is not None:
|
|
453
|
+
i = end
|
|
454
|
+
else:
|
|
455
|
+
# a comma before the closing bracket of its list or object goes
|
|
456
|
+
if not (ch == "," and next_token(i + 1) in ("}", "]")):
|
|
457
|
+
out.append(ch)
|
|
458
|
+
i += 1
|
|
459
|
+
return "".join(out)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def detect_rush(
|
|
463
|
+
tree: set[str], manifests: dict[str, str], warnings: list[str]
|
|
464
|
+
) -> Optional[list[dict]]:
|
|
465
|
+
"""Rush: rush.json's `projects[]`, each a `projectFolder` with a
|
|
466
|
+
package.json in the tree, named by its `packageName`. A rush.json the
|
|
467
|
+
tree lists without its content is named in detect()'s warnings."""
|
|
468
|
+
content = manifests.get("rush.json")
|
|
469
|
+
if content is None:
|
|
470
|
+
return None
|
|
471
|
+
try:
|
|
472
|
+
data = json.loads(_strip_json_comments(content))
|
|
473
|
+
except json.JSONDecodeError as e:
|
|
474
|
+
warnings.append(f"rush.json JSON parse error: {e}")
|
|
475
|
+
return None
|
|
476
|
+
projects = data.get("projects") if isinstance(data, dict) else None
|
|
477
|
+
if not isinstance(projects, list):
|
|
478
|
+
return None
|
|
479
|
+
out: list[dict] = []
|
|
480
|
+
seen: set[str] = set()
|
|
481
|
+
for project in projects:
|
|
482
|
+
if not isinstance(project, dict) or not isinstance(project.get("projectFolder"), str):
|
|
483
|
+
continue
|
|
484
|
+
folder = _normalise_path(project["projectFolder"].strip())
|
|
485
|
+
manifest = f"{folder}/package.json"
|
|
486
|
+
if not folder or folder in seen or manifest not in tree:
|
|
487
|
+
continue
|
|
488
|
+
seen.add(folder)
|
|
489
|
+
name = project.get("packageName")
|
|
490
|
+
if not isinstance(name, str) or not name:
|
|
491
|
+
name = _read_workspace_name(manifest, manifests, folder.rsplit("/", 1)[-1])
|
|
492
|
+
out.append({"name": name, "path": folder, "manifest": manifest})
|
|
493
|
+
return sorted(out, key=lambda ws: ws["path"])
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def detect_nx(
|
|
497
|
+
tree: set[str], manifests: dict[str, str], warnings: list[str]
|
|
498
|
+
) -> Optional[list[dict]]:
|
|
499
|
+
"""Nx: with nx.json at the root, each folder below it holding a
|
|
500
|
+
project.json is a project, named by that file's `name` when its
|
|
501
|
+
content is supplied. An Nx repository that declares its projects
|
|
502
|
+
through package.json workspaces is found by the npm or pnpm detector."""
|
|
503
|
+
if "nx.json" not in tree and "nx.json" not in manifests:
|
|
504
|
+
return None
|
|
505
|
+
out: list[dict] = []
|
|
506
|
+
for path in sorted(tree):
|
|
507
|
+
if not path.endswith("/project.json"):
|
|
508
|
+
continue
|
|
509
|
+
folder = path[: -len("/project.json")]
|
|
510
|
+
if "node_modules" in folder.split("/"):
|
|
511
|
+
continue
|
|
512
|
+
name = folder.rsplit("/", 1)[-1]
|
|
513
|
+
content = manifests.get(path)
|
|
514
|
+
if content is not None:
|
|
515
|
+
try:
|
|
516
|
+
declared = json.loads(content).get("name")
|
|
517
|
+
except (json.JSONDecodeError, AttributeError):
|
|
518
|
+
declared = None
|
|
519
|
+
if isinstance(declared, str) and declared:
|
|
520
|
+
name = declared
|
|
521
|
+
out.append({"name": name, "path": folder, "manifest": path})
|
|
522
|
+
return out
|
|
523
|
+
|
|
524
|
+
|
|
289
525
|
def detect_cargo_workspace(
|
|
290
526
|
tree: set[str], manifests: dict[str, str], warnings: list[str]
|
|
291
527
|
) -> Optional[list[dict]]:
|
|
@@ -356,6 +592,8 @@ DETECTORS: list[tuple[str, callable]] = [
|
|
|
356
592
|
("npm-workspaces", detect_npm_workspaces),
|
|
357
593
|
("pnpm-workspaces", detect_pnpm_workspaces),
|
|
358
594
|
("lerna", detect_lerna),
|
|
595
|
+
("rush", detect_rush),
|
|
596
|
+
("nx", detect_nx),
|
|
359
597
|
("cargo-workspace", detect_cargo_workspace),
|
|
360
598
|
("python-multi-package", detect_python_multi_package),
|
|
361
599
|
("generic-folders", detect_generic_folders),
|
|
@@ -369,6 +607,8 @@ ECOSYSTEM_OF_KIND = {
|
|
|
369
607
|
"npm-workspaces": "js",
|
|
370
608
|
"pnpm-workspaces": "js",
|
|
371
609
|
"lerna": "js",
|
|
610
|
+
"rush": "js",
|
|
611
|
+
"nx": "js",
|
|
372
612
|
"cargo-workspace": "rust",
|
|
373
613
|
"python-multi-package": "python",
|
|
374
614
|
}
|
|
@@ -377,6 +617,8 @@ ROOT_MANIFEST_OF_KIND = {
|
|
|
377
617
|
"npm-workspaces": "package.json",
|
|
378
618
|
"pnpm-workspaces": "pnpm-workspace.yaml",
|
|
379
619
|
"lerna": "lerna.json",
|
|
620
|
+
"rush": "rush.json",
|
|
621
|
+
"nx": "nx.json",
|
|
380
622
|
"cargo-workspace": "Cargo.toml",
|
|
381
623
|
"python-multi-package": None, # discovered from the tree; no single root manifest
|
|
382
624
|
}
|
|
@@ -430,6 +672,12 @@ def detect(payload: dict) -> dict:
|
|
|
430
672
|
if not isinstance(manifests, dict):
|
|
431
673
|
return {"_payload_error": "missing or non-dict 'manifests' field"}
|
|
432
674
|
tree: set[str] = {_normalise_path(p) for p in tree_raw if isinstance(p, str) and p.strip()}
|
|
675
|
+
for name in CONTENT_MANIFESTS:
|
|
676
|
+
if name in tree and name not in manifests:
|
|
677
|
+
warnings.append(
|
|
678
|
+
f"{name} is in the tree but its content was not supplied: pass it under manifests "
|
|
679
|
+
"(or with --manifest-dir) so the detectors can read it"
|
|
680
|
+
)
|
|
433
681
|
|
|
434
682
|
for kind, detector in DETECTORS:
|
|
435
683
|
result = detector(tree, manifests, warnings)
|
|
@@ -450,28 +698,256 @@ def detect(payload: dict) -> dict:
|
|
|
450
698
|
}
|
|
451
699
|
|
|
452
700
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
701
|
+
# --------------------------------------------------------------------------
|
|
702
|
+
# Tree snapshot
|
|
703
|
+
# --------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _module_root(files: list[str], dirs: set[str]) -> str:
|
|
707
|
+
"""The folder whose own folders are a snapshot's module candidates,
|
|
708
|
+
relative to the folder the snapshot describes: its `src/`, else its
|
|
709
|
+
`lib/`, else the folder itself, followed down while it holds no file
|
|
710
|
+
and exactly one folder (hidden ones and NON_MODULE_DIRS aside), which
|
|
711
|
+
holds folders of its own (a wrapper such as a Python `src/<package>/`,
|
|
712
|
+
never a lone `src/components/`)."""
|
|
713
|
+
children: dict[str, list[str]] = {}
|
|
714
|
+
for folder in dirs:
|
|
715
|
+
name = posixpath.basename(folder)
|
|
716
|
+
if not name.startswith(".") and name.lower() not in NON_MODULE_DIRS:
|
|
717
|
+
children.setdefault(posixpath.dirname(folder), []).append(folder)
|
|
718
|
+
holds_files = {posixpath.dirname(f) for f in files}
|
|
719
|
+
base = next((d for d in ("src", "lib") if d in dirs), "")
|
|
720
|
+
while True:
|
|
721
|
+
found = children.get(base, [])
|
|
722
|
+
if len(found) != 1 or base in holds_files or not children.get(found[0]):
|
|
723
|
+
return base
|
|
724
|
+
base = found[0]
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def snapshot(tree: list, manifests: dict, root: str = "", truncated: bool = False) -> dict:
|
|
728
|
+
"""The facts the tree gives an analysis of `root` ("" for the
|
|
729
|
+
repository): see "Tree snapshot" in the module docstring. Raises
|
|
730
|
+
ValueError when the tree lists no file under a `root` given, and
|
|
731
|
+
ImportError when skf-detect-language.py or skf-recommend-scope-type.py
|
|
732
|
+
does not sit beside this script."""
|
|
733
|
+
root = _normalise_path(root.strip())
|
|
734
|
+
root = "" if root == "." else root
|
|
735
|
+
prefix = f"{root}/" if root else ""
|
|
736
|
+
files = sorted({p for p in (_normalise_path(p) for p in tree if isinstance(p, str) and p.strip())
|
|
737
|
+
if p.startswith(prefix)})
|
|
738
|
+
if root and not files:
|
|
739
|
+
raise ValueError(f"the tree lists no file under --root {root}")
|
|
740
|
+
is_source = _sibling("skf-detect-language.py").is_source_file
|
|
741
|
+
rel = [p[len(prefix):] for p in files]
|
|
742
|
+
dirs = {"/".join(parts[:depth]) for parts in (r.split("/") for r in rel) for depth in range(1, len(parts))}
|
|
743
|
+
detected = {"manifest_kind": None, "workspaces": [], "warnings": []}
|
|
744
|
+
if not root:
|
|
745
|
+
detected = detect({"tree": files, "manifests": manifests})
|
|
746
|
+
base = _module_root(rel, dirs)
|
|
747
|
+
# (files, source files) under each folder directly in the module root
|
|
748
|
+
counts: dict[str, list[int]] = {}
|
|
749
|
+
for path in rel:
|
|
750
|
+
rest = path[len(base) + 1:] if base else path
|
|
751
|
+
if (base and not path.startswith(base + "/")) or "/" not in rest or rest.startswith("."):
|
|
752
|
+
continue
|
|
753
|
+
tally = counts.setdefault(rest.split("/", 1)[0], [0, 0])
|
|
754
|
+
tally[0] += 1
|
|
755
|
+
tally[1] += int(is_source(path))
|
|
756
|
+
module_root = prefix + base if base else root
|
|
757
|
+
return {
|
|
758
|
+
"root": root,
|
|
759
|
+
"truncated": truncated,
|
|
760
|
+
"file_count": len(files),
|
|
761
|
+
"source_file_count": sum(1 for path in rel if is_source(path)),
|
|
762
|
+
"dir_count": len(dirs),
|
|
763
|
+
"top_level_files": sorted(r for r in rel if "/" not in r),
|
|
764
|
+
"top_level_dirs": sorted(d for d in dirs if "/" not in d),
|
|
765
|
+
"manifest_kind": detected["manifest_kind"],
|
|
766
|
+
"workspaces": detected["workspaces"],
|
|
767
|
+
"module_root": module_root,
|
|
768
|
+
"module_candidates": [
|
|
769
|
+
{"path": f"{module_root}/{name}" if module_root else name,
|
|
770
|
+
"file_count": tally[0], "source_file_count": tally[1]}
|
|
771
|
+
for name, tally in sorted(counts.items())
|
|
772
|
+
],
|
|
773
|
+
"registry_candidates": _sibling("skf-recommend-scope-type.py")._find_registry_files(files),
|
|
774
|
+
"warnings": detected["warnings"],
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
# --------------------------------------------------------------------------
|
|
779
|
+
# Files: the tree listing, the manifests, the sibling scripts
|
|
780
|
+
# --------------------------------------------------------------------------
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
_SIBLINGS: dict[str, object] = {}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _sibling(filename: str):
|
|
787
|
+
"""A helper of this folder, loaded once as a module: skf-detect-language.py
|
|
788
|
+
reads a --tree-file listing, and skf-recommend-scope-type.py names the
|
|
789
|
+
component registry files a snapshot lists. Raises ImportError when it
|
|
790
|
+
does not sit beside this script."""
|
|
791
|
+
module = _SIBLINGS.get(filename)
|
|
792
|
+
if module is None:
|
|
793
|
+
path = Path(__file__).resolve().parent / filename
|
|
794
|
+
name = "skf_" + filename.removeprefix("skf-").removesuffix(".py").replace("-", "_")
|
|
795
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
796
|
+
if spec is None or spec.loader is None:
|
|
797
|
+
raise ImportError(f"cannot load {filename} beside {Path(__file__).name}")
|
|
798
|
+
module = importlib.util.module_from_spec(spec)
|
|
799
|
+
try:
|
|
800
|
+
spec.loader.exec_module(module)
|
|
801
|
+
except (OSError, SyntaxError) as exc:
|
|
802
|
+
raise ImportError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
|
|
803
|
+
_SIBLINGS[filename] = module
|
|
804
|
+
return module
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def read_manifest_dir(folder: str, paths: list[str]) -> dict[str, str]:
|
|
808
|
+
"""{repo path: text} of each of `paths` read from `folder`, a folder laid
|
|
809
|
+
out like the repository. A path outside the repository, or a file
|
|
810
|
+
missing there, larger than MAX_MANIFEST_BYTES or not UTF-8, is left
|
|
811
|
+
out: its content was not supplied."""
|
|
812
|
+
out: dict[str, str] = {}
|
|
813
|
+
for rel in dict.fromkeys(paths):
|
|
814
|
+
if rel.startswith("/") or ".." in rel.split("/"):
|
|
815
|
+
continue
|
|
816
|
+
path = os.path.join(folder, *rel.split("/"))
|
|
817
|
+
try:
|
|
818
|
+
if os.path.getsize(path) > MAX_MANIFEST_BYTES:
|
|
819
|
+
continue
|
|
820
|
+
with open(path, "rb") as fh:
|
|
821
|
+
out[rel] = fh.read().decode("utf-8-sig")
|
|
822
|
+
except (OSError, UnicodeDecodeError):
|
|
823
|
+
continue
|
|
824
|
+
return out
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _listed(tree: object) -> set[str]:
|
|
828
|
+
return {_normalise_path(p) for p in tree if isinstance(p, str)} if isinstance(tree, list) else set()
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _fail(message: str, code: int) -> int:
|
|
832
|
+
print(json.dumps({"error": message}), file=sys.stderr)
|
|
833
|
+
return code
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _force_utf8(*streams) -> None:
|
|
837
|
+
"""Reconfigure the streams to UTF-8, keeping each one's error handler: a
|
|
838
|
+
default Windows console reads stdin as cp1252, which cannot carry a
|
|
839
|
+
UTF-8 payload or tree listing. For stdin this must run before the first
|
|
840
|
+
read. Skips in-process test doubles without reconfigure()."""
|
|
841
|
+
for stream in streams:
|
|
842
|
+
if hasattr(stream, "reconfigure"):
|
|
843
|
+
errors = getattr(stream, "errors", None)
|
|
844
|
+
if errors is None:
|
|
845
|
+
stream.reconfigure(encoding="utf-8")
|
|
846
|
+
else:
|
|
847
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
457
848
|
|
|
458
|
-
raw = args.json
|
|
459
|
-
if raw is None:
|
|
460
|
-
raw = sys.stdin.read()
|
|
461
|
-
if not raw.strip():
|
|
462
|
-
print(json.dumps({"error": "empty input"}), file=sys.stderr)
|
|
463
|
-
return 2
|
|
464
849
|
|
|
850
|
+
def _decode(raw: str) -> tuple[Optional[dict], Optional[str], int]:
|
|
851
|
+
"""(payload, error message, exit code) of a JSON payload's text."""
|
|
852
|
+
if not raw.strip():
|
|
853
|
+
return None, "empty input", 2
|
|
465
854
|
try:
|
|
466
855
|
payload = json.loads(raw)
|
|
467
856
|
except json.JSONDecodeError as e:
|
|
468
|
-
|
|
469
|
-
|
|
857
|
+
return None, f"json decode error: {e}", 2
|
|
858
|
+
if not isinstance(payload, dict):
|
|
859
|
+
return None, "the payload must be a JSON object", 1
|
|
860
|
+
return payload, None, 0
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
864
|
+
_force_utf8(sys.stdin, sys.stdout)
|
|
865
|
+
parser = argparse.ArgumentParser(description="Detect monorepo / workspace layouts from a tree + manifests payload.")
|
|
866
|
+
parser.add_argument("--json", help="Inline JSON payload (otherwise read from stdin).")
|
|
867
|
+
parser.add_argument(
|
|
868
|
+
"--tree-file",
|
|
869
|
+
help="the repository's file list: JSON (a `tree` list, or a list) or one path per line; - reads stdin. "
|
|
870
|
+
"With --tree-file <file>, stdin is not read: pass the manifests with --manifest-dir or --json",
|
|
871
|
+
)
|
|
872
|
+
parser.add_argument(
|
|
873
|
+
"--manifest-dir",
|
|
874
|
+
help="a folder laid out like the repository that holds the manifests (read as `manifests`)",
|
|
875
|
+
)
|
|
876
|
+
parser.add_argument(
|
|
877
|
+
"--manifest-files",
|
|
878
|
+
action="store_true",
|
|
879
|
+
help="print the root manifests of the --tree-file listing whose content the detectors read, "
|
|
880
|
+
"one path per line, and exit",
|
|
881
|
+
)
|
|
882
|
+
parser.add_argument(
|
|
883
|
+
"--snapshot",
|
|
884
|
+
action="store_true",
|
|
885
|
+
help="print the tree snapshot (counts, top-level entries, workspaces, module candidates, registry files) "
|
|
886
|
+
"instead",
|
|
887
|
+
)
|
|
888
|
+
parser.add_argument("--root", help="with --snapshot: the folder to describe (default: the repository)")
|
|
889
|
+
args = parser.parse_args(argv)
|
|
890
|
+
if args.manifest_files and (args.tree_file is None or args.json is not None or args.manifest_dir is not None
|
|
891
|
+
or args.snapshot or args.root is not None):
|
|
892
|
+
return _fail("--manifest-files lists the root manifests of --tree-file and reads nothing else", 2)
|
|
893
|
+
if args.root is not None and not args.snapshot:
|
|
894
|
+
return _fail("--root names the folder --snapshot describes: pass --snapshot", 2)
|
|
895
|
+
|
|
896
|
+
truncated = False
|
|
897
|
+
if args.tree_file is not None:
|
|
898
|
+
try:
|
|
899
|
+
tree, truncated = _sibling("skf-detect-language.py").read_tree_file(args.tree_file)
|
|
900
|
+
except (ImportError, ValueError) as e: # ValueError: the sibling's TreeListingError
|
|
901
|
+
return _fail(str(e), 2)
|
|
902
|
+
if args.manifest_files:
|
|
903
|
+
listed = _listed(tree)
|
|
904
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
905
|
+
sys.stdout.reconfigure(newline="\n") # a shell loop reads the lines: no CR on Windows
|
|
906
|
+
sys.stdout.write("".join(f"{name}\n" for name in CONTENT_MANIFESTS if name in listed))
|
|
907
|
+
return 0
|
|
908
|
+
payload: dict = {}
|
|
909
|
+
if args.json is not None:
|
|
910
|
+
payload, message, code = _decode(args.json)
|
|
911
|
+
if message:
|
|
912
|
+
return _fail(message, code)
|
|
913
|
+
if "tree" in payload:
|
|
914
|
+
return _fail("pass the tree once: in the payload or with --tree-file, not both", 2)
|
|
915
|
+
payload["tree"] = tree
|
|
916
|
+
if args.manifest_dir is None:
|
|
917
|
+
payload.setdefault("manifests", {})
|
|
918
|
+
else:
|
|
919
|
+
payload, message, code = _decode(args.json if args.json is not None else sys.stdin.read())
|
|
920
|
+
if message:
|
|
921
|
+
return _fail(message, code)
|
|
922
|
+
if args.manifest_dir is not None:
|
|
923
|
+
if "manifests" in payload:
|
|
924
|
+
return _fail("pass the manifests once: in the payload or with --manifest-dir, not both", 2)
|
|
925
|
+
listed = _listed(payload.get("tree"))
|
|
926
|
+
payload["manifests"] = read_manifest_dir(args.manifest_dir, [n for n in CONTENT_MANIFESTS if n in listed])
|
|
927
|
+
found = detect(payload)
|
|
928
|
+
if found.get("workspaces"):
|
|
929
|
+
# Each workspace's own manifest names it.
|
|
930
|
+
payload["manifests"].update(
|
|
931
|
+
read_manifest_dir(args.manifest_dir, [ws["manifest"] for ws in found["workspaces"]]))
|
|
932
|
+
|
|
933
|
+
if args.snapshot:
|
|
934
|
+
if not isinstance(payload.get("tree"), list):
|
|
935
|
+
return _fail("missing or non-list 'tree' field", 1)
|
|
936
|
+
if not isinstance(payload.get("manifests"), dict):
|
|
937
|
+
return _fail("missing or non-dict 'manifests' field", 1)
|
|
938
|
+
try:
|
|
939
|
+
out = snapshot(payload["tree"], payload["manifests"], args.root or "", truncated)
|
|
940
|
+
except (ImportError, ValueError) as e: # a missing sibling, or a --root without files
|
|
941
|
+
return _fail(str(e), 2)
|
|
942
|
+
print(json.dumps(out, separators=(",", ":")))
|
|
943
|
+
return 0
|
|
470
944
|
|
|
471
945
|
result = detect(payload)
|
|
472
946
|
if "_payload_error" in result:
|
|
473
|
-
|
|
474
|
-
|
|
947
|
+
return _fail(result["_payload_error"], 1)
|
|
948
|
+
if truncated:
|
|
949
|
+
result["warnings"].append(
|
|
950
|
+
"the --tree-file listing was cut short (truncated): a workspace whose manifest it does not list is missed")
|
|
475
951
|
|
|
476
952
|
print(json.dumps(result, separators=(",", ":")))
|
|
477
953
|
return 0
|