bmad-module-skill-forge 2.1.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 +51 -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 +14 -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 +117 -9
- package/src/shared/scripts/skf-detect-docs.py +892 -4
- 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 +215 -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 +17 -6
- 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 +55 -18
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
- 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 -89
- package/src/skf-setup/references/detect-and-tier.md +58 -66
- 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 -645
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Shape Detect — classify repos into known skill shapes from manifest files.
|
|
@@ -13,7 +13,8 @@ three pipelines.
|
|
|
13
13
|
The five-shape heuristic ladder (apply in order, first match wins):
|
|
14
14
|
|
|
15
15
|
1. language-reference — parser/grammar/language-toolchain project
|
|
16
|
-
Signals:
|
|
16
|
+
Signals: a compiler tree, a grammar file, a package named after a
|
|
17
|
+
parser tool (pest, antlr4, tree-sitter, lark ...)
|
|
17
18
|
2. stack-compose — multi-ecosystem composite project
|
|
18
19
|
Signals: manifests from 2+ distinct ecosystems
|
|
19
20
|
3. reference-app — application, CLI, or demo project
|
|
@@ -22,18 +23,79 @@ The five-shape heuristic ladder (apply in order, first match wins):
|
|
|
22
23
|
Signals: main/module/exports fields, [lib] target, export count
|
|
23
24
|
5. unknown — no heuristic matched
|
|
24
25
|
|
|
26
|
+
A reference-app that rests on a framework dependency alone (no bin) in a
|
|
27
|
+
package that names itself a library also carries the signal
|
|
28
|
+
`app_or_library:framework_dep`: an app built on the framework and a
|
|
29
|
+
library that extends it (FastAPI, a Flask or Django extension, axum-extra)
|
|
30
|
+
declare the same dependencies, so the caller judges which one it is from
|
|
31
|
+
the README or the manifest description. The shape stays reference-app.
|
|
32
|
+
|
|
33
|
+
A parser tool among the runtime dependencies (a consumer: nom, pyparsing,
|
|
34
|
+
tree-sitter ...) decides no shape: a language or DSL built on it and an app
|
|
35
|
+
or library that only reads its input with it declare the same dependency.
|
|
36
|
+
The ladder goes on and its shape carries the signal
|
|
37
|
+
`language_or_user:parser_dep`, so the caller judges which one it is from the
|
|
38
|
+
README or the manifest description. A dev or build dependency is no
|
|
39
|
+
consumer, nor is one in a non-core member (examples/, docs/) or the monorepo
|
|
40
|
+
coordinator root, as with frameworks. Only when the parser dependency is the
|
|
41
|
+
repository's only signal (a bare DSL manifest: no rung answers and no
|
|
42
|
+
manifest declares a bin) does it still give language-reference, in place of
|
|
43
|
+
unknown.
|
|
44
|
+
|
|
25
45
|
CLI:
|
|
26
46
|
uv run src/shared/scripts/skf-shape-detect.py \\
|
|
27
47
|
--repo-url <url> --manifests <path1,path2,...>
|
|
48
|
+
uv run src/shared/scripts/skf-shape-detect.py \\
|
|
49
|
+
--repo-url <url> --manifests <path1,path2,...> --tree-file <file>
|
|
50
|
+
uv run src/shared/scripts/skf-shape-detect.py \\
|
|
51
|
+
--repo-url <url> --manifests-file <scan.json> --manifest-dir <root> \\
|
|
52
|
+
--tree-file <file>
|
|
28
53
|
|
|
29
54
|
Input:
|
|
30
55
|
--repo-url repository URL (required; context only, no cloning)
|
|
31
56
|
--manifests comma-separated local file paths to manifest files (may be
|
|
32
57
|
empty when a tree-level signal is supplied instead)
|
|
58
|
+
--manifests-file the JSON envelope `skf-scan-manifests.py scan <root>`
|
|
59
|
+
printed, in place of --manifests: its `manifests[]`
|
|
60
|
+
entries of the types this script classifies (the _PARSERS
|
|
61
|
+
file names) are read, each `path` resolved against
|
|
62
|
+
--manifest-dir, so no caller filters, resolves or joins
|
|
63
|
+
the manifest list. A manifest's own path below that root
|
|
64
|
+
(not where the root sits) decides whether it is core and
|
|
65
|
+
how deep it is.
|
|
66
|
+
--manifest-dir the folder the scan ran on (required with
|
|
67
|
+
--manifests-file)
|
|
68
|
+
--tree-file the repository's whole file list (`-` reads it from stdin),
|
|
69
|
+
read by skf-detect-language.py's read_tree_file() (the
|
|
70
|
+
sibling in this folder, which must sit beside this
|
|
71
|
+
script), whose --tree-file text lists the listings it
|
|
72
|
+
reads (`git -c core.quotePath=false ls-tree -r --name-only
|
|
73
|
+
HEAD` prints one). The script finds the tree-level signals
|
|
74
|
+
in it itself (tree_signals()), in place of --grammar-files
|
|
75
|
+
and --tree-paths
|
|
33
76
|
--grammar-files comma-separated repo-relative grammar files (*.y, *.g4,
|
|
34
77
|
*.pest, Grammar/python.gram, ...); a whole-language signal
|
|
35
78
|
--tree-paths comma-separated repo-relative directory/structural signals
|
|
36
79
|
harvested from the clone (compiler/ dir, lexer/parser/ast)
|
|
80
|
+
Passing --tree-file with --grammar-files or --tree-paths is an error, and
|
|
81
|
+
so is passing --manifests with --manifests-file.
|
|
82
|
+
|
|
83
|
+
tree_signals() reads every file of the listing and every folder that holds
|
|
84
|
+
one, so no caller's filter drops a path the gates accept (a root
|
|
85
|
+
`grammar.js`, `Python/ceval.c`, `check.go`), but leaves out a path with a
|
|
86
|
+
hidden segment or a segment of _NON_CORE_PATH_SEGMENTS (tests and fixtures,
|
|
87
|
+
docs, examples, benchmarks, scripts and tools): a CI workflow's check.yml,
|
|
88
|
+
scripts/check.sh or a test fixture's vm.js is not the repository's own
|
|
89
|
+
code. The grammar files are the paths _is_grammar_file() accepts at most
|
|
90
|
+
_GRAMMAR_MAX_DEPTH segments deep (a grammar deeper in the tree is a vendored
|
|
91
|
+
or test fixture) and outside a parsing folder below the top of the tree (a
|
|
92
|
+
library's reader of an input format, such as sympy/parsing/latex/LaTeX.g4,
|
|
93
|
+
_FORMAT_PARSER_DIRS), and the tree paths are every folder (with a trailing /)
|
|
94
|
+
and every file, which the compiler-folder, triad and corroborating-member
|
|
95
|
+
gates of _whole_language_tree() then judge: the triad among the paths near a
|
|
96
|
+
compiler folder, the corroborating member anywhere. A listing that says it
|
|
97
|
+
was cut short (a GitHub tree's `truncated`) adds the signal
|
|
98
|
+
`tree_truncated`.
|
|
37
99
|
|
|
38
100
|
Output (JSON on stdout):
|
|
39
101
|
shape library-API | reference-app | language-reference
|
|
@@ -46,25 +108,26 @@ Output (JSON on stdout):
|
|
|
46
108
|
Exit codes:
|
|
47
109
|
0 shape classified (not unknown)
|
|
48
110
|
1 unknown shape (no heuristic matched)
|
|
49
|
-
2 error (invalid args, missing/unreadable files,
|
|
111
|
+
2 error (invalid args, missing/unreadable files, a tree listing that
|
|
112
|
+
cannot be read, holds no path or reports a failure, a manifests file
|
|
113
|
+
that is not a scan envelope, parse failure)
|
|
50
114
|
"""
|
|
51
115
|
|
|
52
116
|
from __future__ import annotations
|
|
53
117
|
|
|
54
118
|
import argparse
|
|
119
|
+
import importlib.util
|
|
55
120
|
import json
|
|
56
121
|
import re
|
|
57
122
|
import sys
|
|
123
|
+
import tomllib
|
|
58
124
|
from pathlib import Path
|
|
59
125
|
from typing import Any
|
|
60
126
|
|
|
61
|
-
try:
|
|
62
|
-
import tomllib
|
|
63
|
-
except ImportError:
|
|
64
|
-
tomllib = None # type: ignore[assignment]
|
|
65
|
-
|
|
66
127
|
# ---------------------------------------------------------------------------
|
|
67
|
-
# Parser/grammar
|
|
128
|
+
# Parser/grammar tools: a package named after one is a language-reference
|
|
129
|
+
# producer; one among the runtime deps raises the language_or_user:parser_dep
|
|
130
|
+
# question (detect() below)
|
|
68
131
|
# ---------------------------------------------------------------------------
|
|
69
132
|
|
|
70
133
|
_PARSER_DEPS_NPM = frozenset({
|
|
@@ -100,6 +163,17 @@ _GRAMMAR_EXTS = frozenset({
|
|
|
100
163
|
".ungram",
|
|
101
164
|
})
|
|
102
165
|
_GRAMMAR_BASENAMES = frozenset({"grammar.js", "grammar.json", "python.gram"})
|
|
166
|
+
# A grammar file deeper than this many path segments is a vendored or test
|
|
167
|
+
# fixture, not the repo's own grammar (Grammar/python.gram is 2 deep).
|
|
168
|
+
_GRAMMAR_MAX_DEPTH = 4
|
|
169
|
+
# A library keeps its readers of other formats in a parsing folder of its own
|
|
170
|
+
# package: sympy's sympy/parsing/latex/LaTeX.g4 and
|
|
171
|
+
# sympy/parsing/autolev/Autolev.g4 sit beside sympy/core/ and the library's
|
|
172
|
+
# other modules. A grammar below such a folder parses an input the library
|
|
173
|
+
# reads, not the language the repository implements. A parsing folder at the
|
|
174
|
+
# top of the tree is not one of these: there the parser is the repository's
|
|
175
|
+
# own.
|
|
176
|
+
_FORMAT_PARSER_DIRS = frozenset({"parsing", "parsers"})
|
|
103
177
|
|
|
104
178
|
# Concrete parsers a repo CONSUMES. If a repo's own runtime deps contain one of
|
|
105
179
|
# these it delegates parsing — a formatter/linter/bundler, never a
|
|
@@ -247,9 +321,14 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
|
|
|
247
321
|
``_COMPILER_DIRS_CASE`` (case-sensitive ``Parser``). A file named
|
|
248
322
|
``Parser.js`` or ``compiler.dart`` never satisfies this; a bare
|
|
249
323
|
``src/`` / ``lib/`` / ``src/language/`` directory never does either.
|
|
250
|
-
(D) a lexer+parser+AST triad, parser MANDATORY, at least 2 of 3 present
|
|
251
|
-
|
|
252
|
-
|
|
324
|
+
(D) a lexer+parser+AST triad, parser MANDATORY, at least 2 of 3 present,
|
|
325
|
+
among the members NEAR that compiler directory (_near_compiler_dir):
|
|
326
|
+
a large library's lexer and parser in an unrelated corner of the
|
|
327
|
+
tree (PyTorch's TorchScript frontend beside its torch/compiler/ API
|
|
328
|
+
package) are no compiler's triad.
|
|
329
|
+
(W) a corroborating codegen / VM / type-checker member anywhere in the
|
|
330
|
+
tree, so a markdown or CSS library (lexer+parser+AST only) does not
|
|
331
|
+
qualify.
|
|
253
332
|
|
|
254
333
|
Gates G (delegating consumer) and L (markup identity) depend on manifest
|
|
255
334
|
data and are applied by the caller.
|
|
@@ -258,15 +337,13 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
|
|
|
258
337
|
return None
|
|
259
338
|
|
|
260
339
|
compiler_dirs: list[str] = []
|
|
261
|
-
|
|
262
|
-
basenames: set[str] = set()
|
|
340
|
+
members_of: list[tuple[str, str, str]] = [] # (path, basename, stem)
|
|
263
341
|
for tp in tree_paths:
|
|
264
342
|
norm = tp.rstrip("/")
|
|
265
343
|
if not norm:
|
|
266
344
|
continue
|
|
267
345
|
base = norm.rsplit("/", 1)[-1]
|
|
268
|
-
|
|
269
|
-
stems.add(base.rsplit(".", 1)[0].lower() if "." in base else base.lower())
|
|
346
|
+
members_of.append((norm, base, base.rsplit(".", 1)[0].lower() if "." in base else base.lower()))
|
|
270
347
|
if tp.endswith("/"):
|
|
271
348
|
low = norm.lower()
|
|
272
349
|
if any(low == m or low.endswith("/" + m) for m in _COMPILER_DIRS) or \
|
|
@@ -277,183 +354,101 @@ def _whole_language_tree(tree_paths: list[str]) -> tuple[str, str] | None:
|
|
|
277
354
|
if not compiler_dirs:
|
|
278
355
|
return None
|
|
279
356
|
|
|
280
|
-
# (
|
|
281
|
-
|
|
282
|
-
parser = bool(stems & _PARSER_STEMS) or "rustc_parse" in basenames
|
|
283
|
-
binder, checker = "binder" in stems, "checker" in stems
|
|
284
|
-
ast = (bool(stems & _AST_STEMS) or "rustc_ast" in basenames
|
|
285
|
-
or (binder and checker))
|
|
286
|
-
if not parser or (lexer + parser + ast) < 2:
|
|
287
|
-
return None
|
|
288
|
-
|
|
289
|
-
# (W) — corroborating compiler-grade member
|
|
357
|
+
# (W): a corroborating compiler-grade member, anywhere in the tree
|
|
358
|
+
stems = {stem for _, _, stem in members_of}
|
|
290
359
|
w = bool(stems & (_CODEGEN_STEMS | _VM_STEMS | _CHECK_STEMS)) or \
|
|
291
|
-
any(
|
|
360
|
+
any(base.startswith("rustc_codegen") for _, base, _ in members_of)
|
|
292
361
|
if not w:
|
|
293
362
|
return None
|
|
294
363
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
buf.append(ch)
|
|
320
|
-
continue
|
|
321
|
-
if ch in ("[", "{"):
|
|
322
|
-
depth += 1
|
|
323
|
-
buf.append(ch)
|
|
324
|
-
continue
|
|
325
|
-
if ch in ("]", "}"):
|
|
326
|
-
depth -= 1
|
|
327
|
-
buf.append(ch)
|
|
328
|
-
continue
|
|
329
|
-
if ch == sep and depth == 0:
|
|
330
|
-
parts.append("".join(buf))
|
|
331
|
-
buf = []
|
|
332
|
-
continue
|
|
333
|
-
buf.append(ch)
|
|
334
|
-
if buf:
|
|
335
|
-
parts.append("".join(buf))
|
|
336
|
-
return parts
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def _decode_toml_value(raw: str) -> Any:
|
|
340
|
-
s = raw.strip()
|
|
341
|
-
if not s:
|
|
342
|
-
return ""
|
|
343
|
-
if len(s) >= 2 and s[0] == '"' and s[-1] == '"':
|
|
344
|
-
return s[1:-1]
|
|
345
|
-
if len(s) >= 2 and s[0] == "'" and s[-1] == "'":
|
|
346
|
-
return s[1:-1]
|
|
347
|
-
if s == "true":
|
|
364
|
+
# (D): a triad near a compiler directory, parser mandatory, >= 2 of 3
|
|
365
|
+
for compiler_dir in compiler_dirs:
|
|
366
|
+
near = [(base, stem) for path, base, stem in members_of if _near_compiler_dir(path, compiler_dir)]
|
|
367
|
+
stems = {stem for _, stem in near}
|
|
368
|
+
basenames = {base for base, _ in near}
|
|
369
|
+
lexer = bool(stems & _LEXER_STEMS) or "rustc_lexer" in basenames
|
|
370
|
+
parser = bool(stems & _PARSER_STEMS) or "rustc_parse" in basenames
|
|
371
|
+
ast = (bool(stems & _AST_STEMS) or "rustc_ast" in basenames
|
|
372
|
+
or ("binder" in stems and "checker" in stems))
|
|
373
|
+
if parser and (lexer + parser + ast) >= 2:
|
|
374
|
+
members = ",".join(
|
|
375
|
+
m for m, present in (("lexer", lexer), ("parser", parser), ("ast", ast))
|
|
376
|
+
if present
|
|
377
|
+
)
|
|
378
|
+
return compiler_dir, members
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _near_compiler_dir(path: str, compiler_dir: str) -> bool:
|
|
383
|
+
"""Whether a tree path is near a compiler directory, for gate D: the
|
|
384
|
+
directory itself or a path under it, or a path at most two levels below
|
|
385
|
+
the directory that holds it (CPython's Parser/ has Python/ast.c beside
|
|
386
|
+
it, the Go port's internal/compiler/ has internal/parser/parser.go)."""
|
|
387
|
+
if path == compiler_dir or path.startswith(compiler_dir + "/"):
|
|
348
388
|
return True
|
|
349
|
-
if
|
|
389
|
+
parent = compiler_dir.rsplit("/", 1)[0] if "/" in compiler_dir else ""
|
|
390
|
+
if parent and not path.startswith(parent + "/"):
|
|
350
391
|
return False
|
|
351
|
-
if
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
for
|
|
371
|
-
|
|
372
|
-
if "=" in pair:
|
|
373
|
-
k, _, v = pair.partition("=")
|
|
374
|
-
result[k.strip()] = _decode_toml_value(v.strip())
|
|
375
|
-
return result
|
|
376
|
-
try:
|
|
377
|
-
return int(s) if "." not in s else float(s)
|
|
378
|
-
except ValueError:
|
|
379
|
-
return s
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _loads_toml_fallback(content: str) -> dict[str, Any]:
|
|
383
|
-
"""Parse the TOML subset found in pyproject.toml / Cargo.toml."""
|
|
384
|
-
root: dict[str, Any] = {}
|
|
385
|
-
current = root
|
|
386
|
-
lines = content.split("\n")
|
|
387
|
-
i = 0
|
|
388
|
-
while i < len(lines):
|
|
389
|
-
raw = lines[i]
|
|
390
|
-
i += 1
|
|
391
|
-
stripped = raw.strip()
|
|
392
|
-
if not stripped or stripped[0] == "#":
|
|
392
|
+
return path[len(parent) + 1 if parent else 0:].count("/") <= 1
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def tree_signals(files: list[str]) -> tuple[list[str], list[str]]:
|
|
396
|
+
"""(grammar files, tree paths) of a repository's whole file list, the
|
|
397
|
+
tree-level inputs of detect(): the grammar files _is_grammar_file()
|
|
398
|
+
accepts at most _GRAMMAR_MAX_DEPTH segments deep and not below a
|
|
399
|
+
_FORMAT_PARSER_DIRS folder under the top one, and every folder (with
|
|
400
|
+
a trailing /) then every file as tree paths, so the gates of
|
|
401
|
+
_whole_language_tree(), not a name filter, decide what counts. A path
|
|
402
|
+
with a hidden segment or a segment of _NON_CORE_PATH_SEGMENTS (tests and
|
|
403
|
+
fixtures, docs, examples, benchmarks, scripts and tools) is left out of
|
|
404
|
+
both: a CI workflow, a script or a test fixture is not the repository's
|
|
405
|
+
own code. An entry that ends in / is a folder."""
|
|
406
|
+
grammar: set[str] = set()
|
|
407
|
+
dirs: set[str] = set()
|
|
408
|
+
names: set[str] = set()
|
|
409
|
+
for raw in files:
|
|
410
|
+
entry = raw.strip()
|
|
411
|
+
parts = [p for p in entry.split("/") if p and p != "."]
|
|
412
|
+
if not parts or any(p.startswith(".") or p.lower() in _NON_CORE_PATH_SEGMENTS for p in parts):
|
|
393
413
|
continue
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
# key = value
|
|
429
|
-
eq = stripped.find("=")
|
|
430
|
-
if eq < 0:
|
|
431
|
-
continue
|
|
432
|
-
key = stripped[:eq].strip().strip('"')
|
|
433
|
-
val_str = stripped[eq + 1:].strip()
|
|
434
|
-
|
|
435
|
-
# Remove trailing comment outside strings
|
|
436
|
-
if val_str and val_str[0] not in ('"', "'", "[", "{"):
|
|
437
|
-
ci = val_str.find(" #")
|
|
438
|
-
if ci >= 0:
|
|
439
|
-
val_str = val_str[:ci].strip()
|
|
440
|
-
|
|
441
|
-
# Multi-line array
|
|
442
|
-
if val_str.startswith("[") and "]" not in val_str:
|
|
443
|
-
while i < len(lines):
|
|
444
|
-
val_str += " " + lines[i].strip()
|
|
445
|
-
i += 1
|
|
446
|
-
if "]" in val_str:
|
|
447
|
-
break
|
|
448
|
-
|
|
449
|
-
current[key] = _decode_toml_value(val_str)
|
|
450
|
-
return root
|
|
414
|
+
path = "/".join(parts)
|
|
415
|
+
if entry.endswith("/"):
|
|
416
|
+
dirs.add(path + "/")
|
|
417
|
+
else:
|
|
418
|
+
names.add(path)
|
|
419
|
+
if (len(parts) <= _GRAMMAR_MAX_DEPTH and _is_grammar_file(path)
|
|
420
|
+
and not any(p.lower() in _FORMAT_PARSER_DIRS for p in parts[1:-1])):
|
|
421
|
+
grammar.add(path)
|
|
422
|
+
for depth in range(1, len(parts)):
|
|
423
|
+
dirs.add("/".join(parts[:depth]) + "/")
|
|
424
|
+
return sorted(grammar), sorted(dirs) + sorted(names)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
_SIBLINGS: dict[str, Any] = {}
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _sibling(filename: str) -> Any:
|
|
431
|
+
"""A helper of this folder, loaded once as a module: skf-detect-language.py
|
|
432
|
+
reads a --tree-file listing. Raises ImportError when it does not sit
|
|
433
|
+
beside this script."""
|
|
434
|
+
module = _SIBLINGS.get(filename)
|
|
435
|
+
if module is None:
|
|
436
|
+
path = Path(__file__).resolve().parent / filename
|
|
437
|
+
name = "skf_" + filename.removeprefix("skf-").removesuffix(".py").replace("-", "_")
|
|
438
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
439
|
+
if spec is None or spec.loader is None:
|
|
440
|
+
raise ImportError(f"cannot load {filename} beside {Path(__file__).name}")
|
|
441
|
+
module = importlib.util.module_from_spec(spec)
|
|
442
|
+
try:
|
|
443
|
+
spec.loader.exec_module(module)
|
|
444
|
+
except (OSError, SyntaxError) as exc:
|
|
445
|
+
raise ImportError(f"cannot load {filename} beside {Path(__file__).name}: {exc}") from exc
|
|
446
|
+
_SIBLINGS[filename] = module
|
|
447
|
+
return module
|
|
451
448
|
|
|
452
449
|
|
|
453
450
|
def _parse_toml(content: str) -> dict[str, Any]:
|
|
454
|
-
|
|
455
|
-
return tomllib.loads(content)
|
|
456
|
-
return _loads_toml_fallback(content)
|
|
451
|
+
return tomllib.loads(content)
|
|
457
452
|
|
|
458
453
|
|
|
459
454
|
# ---------------------------------------------------------------------------
|
|
@@ -817,6 +812,29 @@ def _parse_manifest(path: Path) -> dict[str, Any]:
|
|
|
817
812
|
return parser(path)
|
|
818
813
|
|
|
819
814
|
|
|
815
|
+
def read_manifests_file(path: str) -> list[str]:
|
|
816
|
+
"""The manifest paths of a skf-scan-manifests.py envelope that this
|
|
817
|
+
script classifies (a _PARSERS file name), in the envelope's order and
|
|
818
|
+
as the envelope spells them: relative to the folder the scan ran on."""
|
|
819
|
+
try:
|
|
820
|
+
envelope = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
821
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
822
|
+
_die(f"cannot read the manifests file {path}: {exc}", "MANIFESTS_FILE_ERROR")
|
|
823
|
+
entries = envelope.get("manifests") if isinstance(envelope, dict) else None
|
|
824
|
+
if not isinstance(entries, list):
|
|
825
|
+
_die(f"{path} is not a skf-scan-manifests.py envelope: it has no manifests list",
|
|
826
|
+
"MANIFESTS_FILE_ERROR")
|
|
827
|
+
paths: list[str] = []
|
|
828
|
+
for entry in entries:
|
|
829
|
+
rel = entry.get("path") if isinstance(entry, dict) else None
|
|
830
|
+
if not isinstance(rel, str) or not rel:
|
|
831
|
+
_die(f"{path}: a manifests entry has no path", "MANIFESTS_FILE_ERROR")
|
|
832
|
+
rel = rel.replace("\\", "/")
|
|
833
|
+
if rel.rsplit("/", 1)[-1] in _PARSERS and rel not in paths:
|
|
834
|
+
paths.append(rel)
|
|
835
|
+
return paths
|
|
836
|
+
|
|
837
|
+
|
|
820
838
|
# ---------------------------------------------------------------------------
|
|
821
839
|
# Core classification
|
|
822
840
|
# ---------------------------------------------------------------------------
|
|
@@ -826,6 +844,7 @@ def detect(
|
|
|
826
844
|
manifest_paths: list[str],
|
|
827
845
|
grammar_files: list[str] | None = None,
|
|
828
846
|
tree_paths: list[str] | None = None,
|
|
847
|
+
manifest_dir: str | None = None,
|
|
829
848
|
) -> dict[str, Any]:
|
|
830
849
|
"""Classify a repo into a skill shape from its manifest files.
|
|
831
850
|
|
|
@@ -834,7 +853,9 @@ def detect(
|
|
|
834
853
|
tree-level signals harvested from the clone; they let whole-language repos
|
|
835
854
|
that carry no parser-generator dependency — and even manifest-less ones —
|
|
836
855
|
be classified. When all three inputs are empty there is nothing to
|
|
837
|
-
classify and we error, exactly as before.
|
|
856
|
+
classify and we error, exactly as before. With `manifest_dir`, each
|
|
857
|
+
manifest path is relative to it: the file read is the joined path, and
|
|
858
|
+
the core and depth checks see the relative one.
|
|
838
859
|
"""
|
|
839
860
|
grammar_files = grammar_files or []
|
|
840
861
|
tree_paths = tree_paths or []
|
|
@@ -843,7 +864,7 @@ def detect(
|
|
|
843
864
|
|
|
844
865
|
parsed: list[dict[str, Any]] = []
|
|
845
866
|
for mp in manifest_paths:
|
|
846
|
-
p = Path(mp)
|
|
867
|
+
p = Path(manifest_dir) / mp if manifest_dir is not None else Path(mp)
|
|
847
868
|
if not p.is_file():
|
|
848
869
|
_die(f"Manifest not found: {p.as_posix()}", "MANIFEST_NOT_FOUND")
|
|
849
870
|
m = _parse_manifest(p)
|
|
@@ -934,15 +955,20 @@ def detect(
|
|
|
934
955
|
# like "parser"/"compiler"/"lang", which are false-positive farms (a CSS
|
|
935
956
|
# parser, compiler-builtins, an arg parser are ordinary libraries).
|
|
936
957
|
#
|
|
937
|
-
# CONSUMER: a project
|
|
938
|
-
#
|
|
939
|
-
#
|
|
958
|
+
# CONSUMER: a project with a parser tool among its core members' RUNTIME
|
|
959
|
+
# deps, read like the framework signals above: a dev or build dependency
|
|
960
|
+
# tests or builds the project, and an examples/ member or the coordinator
|
|
961
|
+
# root is not the product. It may be a language built on the tool (a DSL
|
|
962
|
+
# on lalrpop) or only read its input with it (a CLI on nom), so it
|
|
963
|
+
# decides no shape: the ladder asks (below). Exclude the repo's own
|
|
964
|
+
# producer name from the consumer list so a self-reference isn't
|
|
965
|
+
# double-counted as "uses".
|
|
940
966
|
own_names = {
|
|
941
967
|
(m.get("name") or "").strip().lower() for m in parsed if m.get("name")
|
|
942
968
|
}
|
|
943
969
|
parser_producers = sorted(n for n in own_names if n in _ALL_PARSER_DEPS)
|
|
944
970
|
parser_deps = sorted(
|
|
945
|
-
d for d in
|
|
971
|
+
d for d in core_runtime_deps
|
|
946
972
|
if d.lower() in _ALL_PARSER_DEPS and d.lower() not in parser_producers
|
|
947
973
|
)
|
|
948
974
|
|
|
@@ -1019,17 +1045,22 @@ def detect(
|
|
|
1019
1045
|
return {"shape": "language-reference", "signals": signals,
|
|
1020
1046
|
"confidence": round(confidence, 2), **result_base}
|
|
1021
1047
|
|
|
1022
|
-
# 1b. language-reference
|
|
1023
|
-
|
|
1024
|
-
if parser_producers or parser_deps:
|
|
1025
|
-
# A producer (named itself a grammar tool) is a stronger signal than a
|
|
1026
|
-
# consumer (merely depends on one).
|
|
1027
|
-
base = 0.80 if parser_producers else 0.75
|
|
1048
|
+
# 1b. language-reference: a parser/grammar producer (own name).
|
|
1049
|
+
if parser_producers:
|
|
1028
1050
|
n_sig = len(parser_producers) + len(parser_deps)
|
|
1029
|
-
confidence = _clamp(
|
|
1051
|
+
confidence = _clamp(0.80 + n_sig * 0.05, 0.80, 0.90)
|
|
1030
1052
|
return {"shape": "language-reference", "signals": signals,
|
|
1031
1053
|
"confidence": round(confidence, 2), **result_base}
|
|
1032
1054
|
|
|
1055
|
+
# A consumer dep fits a language built on the parser tool and a project
|
|
1056
|
+
# that only reads its input with it alike: the dependency names alone
|
|
1057
|
+
# cannot tell them apart, so the rungs below answer with their own shape
|
|
1058
|
+
# and this signal hands that question to the caller. Gates G and L hold
|
|
1059
|
+
# here too: a repo they exclude is no language reference either way.
|
|
1060
|
+
parser_question = bool(parser_deps) and not delegating_consumer and not markup_identity
|
|
1061
|
+
if parser_question:
|
|
1062
|
+
signals.append("language_or_user:parser_dep")
|
|
1063
|
+
|
|
1033
1064
|
# 2. stack-compose
|
|
1034
1065
|
if len(ecosystems) > 1:
|
|
1035
1066
|
confidence = _clamp(0.80 + (len(ecosystems) - 2) * 0.05, 0.80, 0.90)
|
|
@@ -1041,6 +1072,13 @@ def detect(
|
|
|
1041
1072
|
# core runtime framework. A single-package repo with a bin is an app.
|
|
1042
1073
|
app_trigger = has_framework if package_count > 1 else (app_has_bin or has_framework)
|
|
1043
1074
|
if app_trigger:
|
|
1075
|
+
# A framework dependency with no bin, in a package that names itself a
|
|
1076
|
+
# library, fits an app built on the framework and a library that
|
|
1077
|
+
# extends it (FastAPI on starlette, axum-extra on axum) alike: the
|
|
1078
|
+
# dependency names alone cannot tell them apart, so the signal hands
|
|
1079
|
+
# that question to the caller and the shape stays reference-app.
|
|
1080
|
+
if not app_has_bin and has_library_structure:
|
|
1081
|
+
signals.append("app_or_library:framework_dep")
|
|
1044
1082
|
strength = (1 if app_has_bin else 0) + (1 if has_framework else 0)
|
|
1045
1083
|
confidence = _clamp(0.80 + (strength - 1) * 0.05, 0.80, 0.90)
|
|
1046
1084
|
return {"shape": "reference-app", "signals": signals,
|
|
@@ -1059,7 +1097,21 @@ def detect(
|
|
|
1059
1097
|
return {"shape": "library-API", "signals": signals,
|
|
1060
1098
|
"confidence": round(confidence, 2), **result_base}
|
|
1061
1099
|
|
|
1062
|
-
# 5.
|
|
1100
|
+
# 5. language-reference: a consumer dep is the repository's only signal (a
|
|
1101
|
+
# bare DSL manifest: no rung above answered and no manifest declares a
|
|
1102
|
+
# bin), so no other shape is left to weigh the question against. A bin
|
|
1103
|
+
# that rung 3 does not count (the bin-only members of a monorepo) is an
|
|
1104
|
+
# app signal all the same: such a repository stays unknown, like its twin
|
|
1105
|
+
# with no parser dependency, and the caller analyzes it interactively.
|
|
1106
|
+
if parser_question and not has_bin:
|
|
1107
|
+
signals.remove("language_or_user:parser_dep")
|
|
1108
|
+
return {"shape": "language-reference", "signals": signals,
|
|
1109
|
+
"confidence": 0.75, **result_base}
|
|
1110
|
+
|
|
1111
|
+
# 6. unknown: the interactive fallback weighs the parser itself, with no
|
|
1112
|
+
# shape for the question to qualify.
|
|
1113
|
+
if parser_question:
|
|
1114
|
+
signals.remove("language_or_user:parser_dep")
|
|
1063
1115
|
return {"shape": "unknown", "signals": signals,
|
|
1064
1116
|
"confidence": 0.0, **result_base}
|
|
1065
1117
|
|
|
@@ -1095,15 +1147,31 @@ def _force_utf8(*streams) -> None:
|
|
|
1095
1147
|
|
|
1096
1148
|
|
|
1097
1149
|
def main(argv: list[str]) -> int:
|
|
1098
|
-
_force_utf8(sys.stdout, sys.stderr)
|
|
1150
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
1099
1151
|
parser = argparse.ArgumentParser(
|
|
1100
1152
|
description="Classify a repo into a known skill shape from its manifest files.",
|
|
1101
1153
|
)
|
|
1102
1154
|
parser.add_argument("--repo-url", required=True, help="Repository URL")
|
|
1103
1155
|
parser.add_argument(
|
|
1104
|
-
"--manifests",
|
|
1156
|
+
"--manifests", default=None,
|
|
1105
1157
|
help="Comma-separated local file paths to manifest files (may be empty "
|
|
1106
|
-
"when --grammar-files or --tree-paths carry the signal)"
|
|
1158
|
+
"when --tree-file, --grammar-files or --tree-paths carry the signal); "
|
|
1159
|
+
"required unless --manifests-file is given",
|
|
1160
|
+
)
|
|
1161
|
+
parser.add_argument(
|
|
1162
|
+
"--manifests-file", default=None,
|
|
1163
|
+
help="The JSON envelope skf-scan-manifests.py scan printed, in place of "
|
|
1164
|
+
"--manifests: its manifests of the types this script classifies, "
|
|
1165
|
+
"each path resolved against --manifest-dir",
|
|
1166
|
+
)
|
|
1167
|
+
parser.add_argument(
|
|
1168
|
+
"--manifest-dir", default=None,
|
|
1169
|
+
help="The folder the scan ran on (required with --manifests-file)",
|
|
1170
|
+
)
|
|
1171
|
+
parser.add_argument(
|
|
1172
|
+
"--tree-file", default=None,
|
|
1173
|
+
help="The repository's whole file list: JSON (a `tree` list, or a list) or "
|
|
1174
|
+
"one path per line; - reads stdin. Replaces --grammar-files and --tree-paths",
|
|
1107
1175
|
)
|
|
1108
1176
|
parser.add_argument(
|
|
1109
1177
|
"--grammar-files", default="",
|
|
@@ -1117,13 +1185,39 @@ def main(argv: list[str]) -> int:
|
|
|
1117
1185
|
)
|
|
1118
1186
|
args = parser.parse_args(argv)
|
|
1119
1187
|
|
|
1120
|
-
|
|
1188
|
+
if args.manifests_file is not None:
|
|
1189
|
+
if args.manifests is not None:
|
|
1190
|
+
_die("pass the manifests once: --manifests or --manifests-file", "INVALID_ARGS")
|
|
1191
|
+
if args.manifest_dir is None:
|
|
1192
|
+
_die("--manifests-file needs --manifest-dir, the folder the scan ran on", "INVALID_ARGS")
|
|
1193
|
+
manifest_paths = read_manifests_file(args.manifests_file)
|
|
1194
|
+
elif args.manifests is None:
|
|
1195
|
+
_die("--manifests or --manifests-file is required", "MISSING_MANIFESTS")
|
|
1196
|
+
else:
|
|
1197
|
+
if args.manifest_dir is not None:
|
|
1198
|
+
_die("--manifest-dir goes with --manifests-file", "INVALID_ARGS")
|
|
1199
|
+
manifest_paths = [p.strip() for p in args.manifests.split(",") if p.strip()]
|
|
1121
1200
|
grammar_files = [p.strip() for p in args.grammar_files.split(",") if p.strip()]
|
|
1122
1201
|
tree_paths = [p.strip() for p in args.tree_paths.split(",") if p.strip()]
|
|
1202
|
+
truncated = False
|
|
1203
|
+
if args.tree_file is not None:
|
|
1204
|
+
if grammar_files or tree_paths:
|
|
1205
|
+
_die("pass the tree once: --tree-file, or --grammar-files and --tree-paths", "INVALID_ARGS")
|
|
1206
|
+
try:
|
|
1207
|
+
files, truncated = _sibling("skf-detect-language.py").read_tree_file(args.tree_file)
|
|
1208
|
+
except (ImportError, ValueError) as exc: # ValueError: the sibling's TreeListingError
|
|
1209
|
+
_die(str(exc), "TREE_FILE_ERROR")
|
|
1210
|
+
grammar_files, tree_paths = tree_signals(files)
|
|
1123
1211
|
if not manifest_paths and not grammar_files and not tree_paths:
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1212
|
+
if args.tree_file is None:
|
|
1213
|
+
_die("--manifests requires at least one path", "MISSING_MANIFESTS")
|
|
1214
|
+
# The listing holds only paths tree_signals() leaves out (hidden,
|
|
1215
|
+
# tests, docs, scripts): no signal to classify.
|
|
1216
|
+
result = {"shape": "unknown", "signals": [], "confidence": 0.0, "export_count": 0, "package_count": 0}
|
|
1217
|
+
else:
|
|
1218
|
+
result = detect(args.repo_url, manifest_paths, grammar_files, tree_paths, args.manifest_dir)
|
|
1219
|
+
if truncated:
|
|
1220
|
+
result["signals"].append("tree_truncated")
|
|
1127
1221
|
json.dump(result, sys.stdout, ensure_ascii=False)
|
|
1128
1222
|
sys.stdout.write("\n")
|
|
1129
1223
|
|