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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Names Present: the provenance entries whose export name a skill package never writes.
|
|
6
|
+
|
|
7
|
+
skf-test-skill credits a provenance entry toward a stack's Export Coverage
|
|
8
|
+
only when the package writes its `export_name`: reconcile-coverage.py
|
|
9
|
+
(`load_doc_text`, `names_present`) joins the text of `SKILL.md` and of each
|
|
10
|
+
`.md` file directly in `references/` (sorted, read as UTF-8, no subfolder)
|
|
11
|
+
and looks for the name in it as a fixed string, case-sensitive, never inside
|
|
12
|
+
a longer identifier: on a side where the name ends in a letter, digit, `_`
|
|
13
|
+
or `$`, the next character may not be one of those, so `get` is not found in
|
|
14
|
+
`target` or `getAll` (skf-test-skill's validate-inventory.py keeps that
|
|
15
|
+
rule; NAME_CHARS here must stay the same). A name holding
|
|
16
|
+
`::` (an impl-block method, `Type::method`) rolls up under its type and is
|
|
17
|
+
never looked up, and an entry with no name is no cited contract.
|
|
18
|
+
create-stack-skill (generate-output section 8) runs this helper on a
|
|
19
|
+
compose-mode map before the commit, so a stack never commits a name
|
|
20
|
+
skf-test-skill will not credit. The rule here is the scorer's:
|
|
21
|
+
test/test-skf-stack-step-rules.py runs both on the same package.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
uv run skf-names-present.py --provenance <provenance-map.json> \\
|
|
25
|
+
--skill-dir <dir> [--drop-absent]
|
|
26
|
+
|
|
27
|
+
Output (stdout):
|
|
28
|
+
{
|
|
29
|
+
"checked": <int>, # entries whose export_name was looked up
|
|
30
|
+
"skipped": <int>, # entries with a `::` name or no name
|
|
31
|
+
"absent": [ # in entries[] order
|
|
32
|
+
{"entry_index": <int>, "export_name": "<name>",
|
|
33
|
+
"source_library": "<library>" | null},
|
|
34
|
+
...
|
|
35
|
+
],
|
|
36
|
+
"dropped": [...], # --drop-absent: the absent entries removed
|
|
37
|
+
"provenance_written": <bool>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
--drop-absent removes every absent entry from the map and rewrites it
|
|
41
|
+
through skf-atomic-write.py beside this script (JSON indented by two
|
|
42
|
+
spaces, non-ASCII kept, a final newline). `absent[]` still lists what the
|
|
43
|
+
lookup found, and `dropped[]` repeats it. `entry_index` is the position in
|
|
44
|
+
the map as it was read.
|
|
45
|
+
|
|
46
|
+
Exit codes:
|
|
47
|
+
0 no entry is absent, or --drop-absent removed every absent entry
|
|
48
|
+
1 an entry is absent (absent[] lists it)
|
|
49
|
+
2 usage, file, JSON or write error (one line on stderr, no JSON)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import re
|
|
57
|
+
import subprocess
|
|
58
|
+
import sys
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
|
|
61
|
+
ATOMIC_WRITE_HELPER = "skf-atomic-write.py"
|
|
62
|
+
ATOMIC_WRITE_TIMEOUT_SEC = 60
|
|
63
|
+
METHOD_SEPARATOR = "::"
|
|
64
|
+
# Characters that continue an identifier in the languages SKF documents (`$`
|
|
65
|
+
# for JS/TS names such as `$state`): skf-test-skill's IDENT_CHARS.
|
|
66
|
+
NAME_CHARS = r"\w$"
|
|
67
|
+
_NAME_CHAR_RE = re.compile(f"[{NAME_CHARS}]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_doc_text(skill_dir: Path) -> str:
|
|
71
|
+
"""The text skf-test-skill looks names up in: SKILL.md, then each .md file
|
|
72
|
+
directly in references/ in sorted order, joined by newlines. Raises
|
|
73
|
+
OSError, or UnicodeDecodeError for a file that is not UTF-8."""
|
|
74
|
+
parts = []
|
|
75
|
+
skill_md = skill_dir / "SKILL.md"
|
|
76
|
+
if skill_md.is_file():
|
|
77
|
+
parts.append(skill_md.read_text(encoding="utf-8"))
|
|
78
|
+
refs = skill_dir / "references"
|
|
79
|
+
if refs.is_dir():
|
|
80
|
+
for ref in sorted(refs.glob("*.md")):
|
|
81
|
+
parts.append(ref.read_text(encoding="utf-8"))
|
|
82
|
+
return "\n".join(parts)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def name_written(name: str, doc_text: str) -> bool:
|
|
86
|
+
"""True when `doc_text` holds `name` as skf-test-skill's scorer finds it."""
|
|
87
|
+
body = re.escape(name)
|
|
88
|
+
if _NAME_CHAR_RE.match(name[0]):
|
|
89
|
+
body = f"(?<![{NAME_CHARS}])" + body
|
|
90
|
+
if _NAME_CHAR_RE.match(name[-1]):
|
|
91
|
+
body += f"(?![{NAME_CHARS}])"
|
|
92
|
+
return re.search(body, doc_text) is not None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def looked_up_name(entry: object) -> str | None:
|
|
96
|
+
"""The name skf-test-skill looks up for an entry, or None when it looks
|
|
97
|
+
up none: no string name, or a `Type::method` name."""
|
|
98
|
+
if not isinstance(entry, dict):
|
|
99
|
+
return None
|
|
100
|
+
name = entry.get("export_name")
|
|
101
|
+
if not isinstance(name, str) or not name or METHOD_SEPARATOR in name:
|
|
102
|
+
return None
|
|
103
|
+
return name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def find_absent(entries: list, doc_text: str) -> tuple[int, list[dict]]:
|
|
107
|
+
"""(entries looked up, the absent ones in entries[] order)."""
|
|
108
|
+
checked, absent = 0, []
|
|
109
|
+
for index, entry in enumerate(entries):
|
|
110
|
+
name = looked_up_name(entry)
|
|
111
|
+
if name is None:
|
|
112
|
+
continue
|
|
113
|
+
checked += 1
|
|
114
|
+
if not name_written(name, doc_text):
|
|
115
|
+
library = entry.get("source_library")
|
|
116
|
+
absent.append({
|
|
117
|
+
"entry_index": index,
|
|
118
|
+
"export_name": name,
|
|
119
|
+
"source_library": library if isinstance(library, str) else None,
|
|
120
|
+
})
|
|
121
|
+
return checked, absent
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def atomic_write(target: Path, data: bytes) -> None:
|
|
125
|
+
"""Write `data` to `target` through skf-atomic-write.py beside this
|
|
126
|
+
script. Raises OSError when the helper is missing or fails."""
|
|
127
|
+
helper = Path(__file__).resolve().parent / ATOMIC_WRITE_HELPER
|
|
128
|
+
if not helper.is_file():
|
|
129
|
+
raise OSError(f"{ATOMIC_WRITE_HELPER} not found beside {Path(__file__).name}; nothing written")
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
[sys.executable, str(helper), "write", "--target", str(target)],
|
|
133
|
+
input=data,
|
|
134
|
+
capture_output=True,
|
|
135
|
+
timeout=ATOMIC_WRITE_TIMEOUT_SEC,
|
|
136
|
+
check=False,
|
|
137
|
+
)
|
|
138
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
139
|
+
raise OSError(f"atomic write of {target} failed: {exc}") from exc
|
|
140
|
+
if result.returncode != 0:
|
|
141
|
+
detail = (result.stderr or b"").decode("utf-8", errors="replace").strip()
|
|
142
|
+
raise OSError(f"atomic write of {target} failed: {detail}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _fail(message: str) -> int:
|
|
146
|
+
print(f"error: {' '.join(message.split())}", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
151
|
+
parser = argparse.ArgumentParser(
|
|
152
|
+
prog="skf-names-present",
|
|
153
|
+
description=(
|
|
154
|
+
"List the provenance entries whose export_name the skill package "
|
|
155
|
+
"never writes, by skf-test-skill's Export Coverage rule "
|
|
156
|
+
"(SKILL.md and references/*.md, case-sensitive substring, `::` "
|
|
157
|
+
"names skipped); with --drop-absent, remove them from the map."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument("--provenance", required=True, help="path to provenance-map.json")
|
|
161
|
+
parser.add_argument(
|
|
162
|
+
"--skill-dir",
|
|
163
|
+
required=True,
|
|
164
|
+
help="skill package folder holding the SKILL.md and references/ to search",
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--drop-absent",
|
|
168
|
+
action="store_true",
|
|
169
|
+
help="remove every absent entry and rewrite the map through skf-atomic-write.py",
|
|
170
|
+
)
|
|
171
|
+
return parser
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main(argv: list[str] | None = None) -> int:
|
|
175
|
+
args = _build_parser().parse_args(argv)
|
|
176
|
+
prov_path = Path(args.provenance)
|
|
177
|
+
skill_dir = Path(args.skill_dir)
|
|
178
|
+
if not prov_path.is_file():
|
|
179
|
+
return _fail(f"provenance map not found: {prov_path}")
|
|
180
|
+
# A wrong folder would read every name as absent, and --drop-absent
|
|
181
|
+
# would then empty the map: require the package's SKILL.md.
|
|
182
|
+
if not (skill_dir / "SKILL.md").is_file():
|
|
183
|
+
return _fail(f"skill dir has no SKILL.md: {skill_dir}")
|
|
184
|
+
try:
|
|
185
|
+
prov = json.loads(prov_path.read_text(encoding="utf-8"))
|
|
186
|
+
except (OSError, ValueError) as exc:
|
|
187
|
+
return _fail(f"cannot read the provenance map {prov_path}: {exc}")
|
|
188
|
+
entries = prov.get("entries") if isinstance(prov, dict) else None
|
|
189
|
+
if not isinstance(entries, list):
|
|
190
|
+
return _fail(f"the provenance map {prov_path} has no entries[] list")
|
|
191
|
+
try:
|
|
192
|
+
doc_text = load_doc_text(skill_dir)
|
|
193
|
+
except (OSError, ValueError) as exc:
|
|
194
|
+
return _fail(f"cannot read the skill package {skill_dir}: {exc}")
|
|
195
|
+
|
|
196
|
+
checked, absent = find_absent(entries, doc_text)
|
|
197
|
+
dropped: list[dict] = []
|
|
198
|
+
if args.drop_absent and absent:
|
|
199
|
+
gone = {item["entry_index"] for item in absent}
|
|
200
|
+
prov["entries"] = [entry for index, entry in enumerate(entries) if index not in gone]
|
|
201
|
+
data = (json.dumps(prov, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
|
|
202
|
+
try:
|
|
203
|
+
atomic_write(prov_path, data)
|
|
204
|
+
except OSError as exc:
|
|
205
|
+
return _fail(str(exc))
|
|
206
|
+
dropped = absent
|
|
207
|
+
|
|
208
|
+
result = {
|
|
209
|
+
"checked": checked,
|
|
210
|
+
"skipped": len(entries) - checked,
|
|
211
|
+
"absent": absent,
|
|
212
|
+
"dropped": dropped,
|
|
213
|
+
"provenance_written": bool(dropped),
|
|
214
|
+
}
|
|
215
|
+
# ASCII escapes: a Windows console's code page cannot encode every name.
|
|
216
|
+
sys.stdout.write(json.dumps(result, indent=2) + "\n")
|
|
217
|
+
return 0 if not absent or dropped else 1
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Pair Intersect — compute file-list intersections between library pairs.
|
|
6
6
|
|
|
7
|
-
`detect-integrations.md` §1 (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
`detect-integrations.md` §1 prunes the N*(N-1)/2 library pairs by
|
|
8
|
+
intersecting the per-library file lists that step 3's `skf-count-imports.py`
|
|
9
|
+
run already produced. At N≈21 this collapses ~210 pairs to a handful of
|
|
10
|
+
non-empty-intersection pairs in typical codebases, and each pair's `files`
|
|
11
|
+
are its co-import files: §2 reads them, with their import lines from step
|
|
12
|
+
3's counts, and greps nothing again.
|
|
13
13
|
|
|
14
14
|
This script extracts that intersection from prose-orchestrated work to a
|
|
15
15
|
deterministic helper. The LLM still composes the user-facing warning when
|
|
@@ -17,12 +17,23 @@ the Top-K cap kicks in — the script just flags `truncated: true` so the
|
|
|
17
17
|
caller knows there's something to warn about.
|
|
18
18
|
|
|
19
19
|
Subcommand:
|
|
20
|
-
intersect --libraries <json-file-or-'-'>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
intersect (--libraries <json-file-or-'-'> | --counts <json-file-or-'-'>)
|
|
21
|
+
[--only a,b] [--top-k N]
|
|
22
|
+
Read the libraries (from the file, or stdin if '-') in one of two
|
|
23
|
+
shapes:
|
|
24
|
+
--libraries [{"name": "<lib>", "files": ["<path>", ...]}, ...]
|
|
25
|
+
where each library entry lists the project files that
|
|
26
|
+
import it (`skf-count-imports.py count --format
|
|
27
|
+
libraries` prints this list).
|
|
28
|
+
--counts the `skf-count-imports.py count` envelope as it is
|
|
29
|
+
(its default `full` format): each `dependencies[]`
|
|
30
|
+
entry, then each `units[]` entry, is a library whose
|
|
31
|
+
files are its `files[]` ({path, line}). Entries that
|
|
32
|
+
share a name are one library, with the first line each
|
|
33
|
+
file imports it on.
|
|
34
|
+
--only names the libraries to pair, comma-separated (trimmed, empty
|
|
35
|
+
items dropped); every other library is left out, and a name the input
|
|
36
|
+
does not hold has no files, so it joins no pair.
|
|
26
37
|
|
|
27
38
|
Emit JSON:
|
|
28
39
|
{
|
|
@@ -34,11 +45,15 @@ Subcommand:
|
|
|
34
45
|
"truncated": false,
|
|
35
46
|
"total_pairs": <full non-empty-intersection count before cap>
|
|
36
47
|
}
|
|
48
|
+
With --counts, each `files` entry is instead
|
|
49
|
+
{"path": "<rel-path-forward-slash>", "line_a": N, "line_b": N}
|
|
50
|
+
where `line_a` and `line_b` are the first lines of that file that
|
|
51
|
+
import `a` and `b`: the co-import files of the pair, with their lines.
|
|
37
52
|
|
|
38
53
|
Pairs are computed for each unordered pair (a, b) with a < b
|
|
39
54
|
lexicographically; only non-empty intersections are emitted. The pairs
|
|
40
55
|
list is sorted by intersection_count DESC, then by (a, b) ASC for
|
|
41
|
-
stable, reproducible ordering.
|
|
56
|
+
stable, reproducible ordering; the files of a pair are sorted by path.
|
|
42
57
|
|
|
43
58
|
Top-K cap: default --top-k 20 (matches detect-integrations.md §1's
|
|
44
59
|
"Top-K cap at 20 with explicit warning"). If the full count exceeds
|
|
@@ -49,11 +64,14 @@ CLI examples:
|
|
|
49
64
|
uv run skf-pair-intersect.py intersect --libraries libs.json
|
|
50
65
|
cat libs.json | uv run skf-pair-intersect.py intersect --libraries -
|
|
51
66
|
uv run skf-pair-intersect.py intersect --libraries libs.json --top-k 50
|
|
67
|
+
uv run skf-count-imports.py count . --deps manifests.json \\
|
|
68
|
+
| uv run skf-pair-intersect.py intersect --counts - --only "react,zustand"
|
|
52
69
|
|
|
53
70
|
Exit codes:
|
|
54
|
-
0
|
|
55
|
-
1
|
|
56
|
-
library entry,
|
|
71
|
+
0 - operation succeeded (including: empty input → empty pairs list)
|
|
72
|
+
1 - user error (bad JSON, missing required `name`/`files` field on any
|
|
73
|
+
library entry, a --counts input that is not the count envelope or
|
|
74
|
+
a file entry that is not a {path, line} object, file not readable)
|
|
57
75
|
"""
|
|
58
76
|
|
|
59
77
|
from __future__ import annotations
|
|
@@ -111,11 +129,62 @@ def validate_libraries(libraries: object) -> list[dict]:
|
|
|
111
129
|
return normalized
|
|
112
130
|
|
|
113
131
|
|
|
132
|
+
def libraries_from_counts(counts: object) -> list[dict]:
|
|
133
|
+
"""The libraries of a `skf-count-imports.py count` envelope.
|
|
134
|
+
|
|
135
|
+
Each `dependencies[]` entry, then each `units[]` entry, becomes a
|
|
136
|
+
{name, files, lines} record: `lines` maps each forward-slash path to the
|
|
137
|
+
first line that imports the library. Entries that share a name merge,
|
|
138
|
+
keeping the lower line. Raises ValueError on any structural defect.
|
|
139
|
+
"""
|
|
140
|
+
if not isinstance(counts, dict) or not isinstance(counts.get("dependencies"), list):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"counts input must be the skf-count-imports.py count envelope "
|
|
143
|
+
"(an object with a `dependencies` array)"
|
|
144
|
+
)
|
|
145
|
+
units = counts.get("units", [])
|
|
146
|
+
if not isinstance(units, list):
|
|
147
|
+
raise ValueError("counts `units` is not an array")
|
|
148
|
+
merged: dict[str, dict[str, int]] = {}
|
|
149
|
+
for idx, entry in enumerate(counts["dependencies"] + units):
|
|
150
|
+
if not isinstance(entry, dict):
|
|
151
|
+
raise ValueError(f"counts entry at index {idx} is not an object: {entry!r}")
|
|
152
|
+
name = entry.get("name")
|
|
153
|
+
if not isinstance(name, str) or not name:
|
|
154
|
+
raise ValueError(f"counts entry at index {idx} missing required `name` field")
|
|
155
|
+
files = entry.get("files")
|
|
156
|
+
if not isinstance(files, list):
|
|
157
|
+
raise ValueError(f"counts entry {name!r} missing required `files` array")
|
|
158
|
+
lines = merged.setdefault(name, {})
|
|
159
|
+
for f_idx, f in enumerate(files):
|
|
160
|
+
line = f.get("line") if isinstance(f, dict) else None
|
|
161
|
+
if (not isinstance(f, dict) or not isinstance(f.get("path"), str)
|
|
162
|
+
or not isinstance(line, int) or isinstance(line, bool)):
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"counts entry {name!r} files[{f_idx}] is not a {{path, line}} object: {f!r}"
|
|
165
|
+
)
|
|
166
|
+
path = f["path"].replace("\\", "/")
|
|
167
|
+
if path not in lines or line < lines[path]:
|
|
168
|
+
lines[path] = line
|
|
169
|
+
return [
|
|
170
|
+
{"name": name, "files": sorted(lines), "lines": lines}
|
|
171
|
+
for name, lines in merged.items()
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def only(libraries: list[dict], names: list[str]) -> list[dict]:
|
|
176
|
+
"""The libraries `names` lists; a listed name with no library adds nothing."""
|
|
177
|
+
keep = set(names)
|
|
178
|
+
return [lib for lib in libraries if lib["name"] in keep]
|
|
179
|
+
|
|
180
|
+
|
|
114
181
|
def compute_pairs(libraries: list[dict]) -> list[dict]:
|
|
115
182
|
"""For each unordered (a, b) with a < b lexicographically, compute the
|
|
116
183
|
file-list intersection. Emit only pairs with non-empty intersections.
|
|
117
184
|
|
|
118
185
|
The returned list is sorted by intersection_count DESC, then (a, b) ASC.
|
|
186
|
+
When both libraries carry `lines` (the --counts input), each file is a
|
|
187
|
+
{path, line_a, line_b} object instead of a path.
|
|
119
188
|
"""
|
|
120
189
|
# sort libraries by name for stable iteration order
|
|
121
190
|
sorted_libs = sorted(libraries, key=lambda lib: lib["name"])
|
|
@@ -135,11 +204,17 @@ def compute_pairs(libraries: list[dict]) -> list[dict]:
|
|
|
135
204
|
intersection = sorted(set_a & set_b)
|
|
136
205
|
if not intersection:
|
|
137
206
|
continue
|
|
207
|
+
files: list = intersection
|
|
208
|
+
if "lines" in a and "lines" in b:
|
|
209
|
+
files = [
|
|
210
|
+
{"path": path, "line_a": a["lines"][path], "line_b": b["lines"][path]}
|
|
211
|
+
for path in intersection
|
|
212
|
+
]
|
|
138
213
|
pairs.append({
|
|
139
214
|
"a": a["name"],
|
|
140
215
|
"b": b["name"],
|
|
141
216
|
"intersection_count": len(intersection),
|
|
142
|
-
"files":
|
|
217
|
+
"files": files,
|
|
143
218
|
})
|
|
144
219
|
pairs.sort(key=lambda p: (-p["intersection_count"], p["a"], p["b"]))
|
|
145
220
|
return pairs
|
|
@@ -167,34 +242,38 @@ def intersect(libraries: list[dict], top_k: int = DEFAULT_TOP_K) -> dict:
|
|
|
167
242
|
# --------------------------------------------------------------------------
|
|
168
243
|
|
|
169
244
|
|
|
170
|
-
def _read_libraries_source(source: str) -> object:
|
|
171
|
-
"""Read
|
|
245
|
+
def _read_libraries_source(source: str, what: str = "libraries") -> object:
|
|
246
|
+
"""Read the input JSON from a file path or stdin (if source == '-')."""
|
|
172
247
|
if source == "-":
|
|
173
248
|
try:
|
|
174
249
|
text = sys.stdin.read()
|
|
175
250
|
except OSError as exc:
|
|
176
|
-
raise ValueError(f"failed to read
|
|
251
|
+
raise ValueError(f"failed to read {what} JSON from stdin: {exc}") from exc
|
|
177
252
|
else:
|
|
178
253
|
path = Path(source)
|
|
179
254
|
if not path.is_file():
|
|
180
|
-
raise ValueError(f"
|
|
255
|
+
raise ValueError(f"{what} file not found: {path}")
|
|
181
256
|
try:
|
|
182
257
|
text = path.read_text(encoding="utf-8")
|
|
183
258
|
except OSError as exc:
|
|
184
|
-
raise ValueError(f"failed to read
|
|
259
|
+
raise ValueError(f"failed to read {what} file {path}: {exc}") from exc
|
|
185
260
|
try:
|
|
186
261
|
return json.loads(text)
|
|
187
262
|
except json.JSONDecodeError as exc:
|
|
188
|
-
raise ValueError(f"malformed JSON in
|
|
263
|
+
raise ValueError(f"malformed JSON in {what} input: {exc}") from exc
|
|
189
264
|
|
|
190
265
|
|
|
191
266
|
def _cmd_intersect(args: argparse.Namespace) -> int:
|
|
192
267
|
try:
|
|
193
|
-
|
|
194
|
-
|
|
268
|
+
if args.counts is not None:
|
|
269
|
+
libraries = libraries_from_counts(_read_libraries_source(args.counts, "counts"))
|
|
270
|
+
else:
|
|
271
|
+
libraries = validate_libraries(_read_libraries_source(args.libraries))
|
|
195
272
|
except ValueError as exc:
|
|
196
273
|
print(f"error: {exc}", file=sys.stderr)
|
|
197
274
|
return 1
|
|
275
|
+
if args.only is not None:
|
|
276
|
+
libraries = only(libraries, [n for n in (part.strip() for part in args.only.split(",")) if n])
|
|
198
277
|
if args.top_k < 0:
|
|
199
278
|
print(f"error: --top-k must be >= 0; got {args.top_k}", file=sys.stderr)
|
|
200
279
|
return 1
|
|
@@ -218,14 +297,26 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
218
297
|
"intersect",
|
|
219
298
|
help="emit non-empty-intersection pairs as JSON, sorted DESC by count",
|
|
220
299
|
)
|
|
221
|
-
p_int.
|
|
300
|
+
source = p_int.add_mutually_exclusive_group(required=True)
|
|
301
|
+
source.add_argument(
|
|
222
302
|
"--libraries",
|
|
223
|
-
required=True,
|
|
224
303
|
help=(
|
|
225
304
|
"path to libraries JSON, or '-' to read from stdin. "
|
|
226
305
|
"Shape: [{\"name\": \"<lib>\", \"files\": [\"<path>\", ...]}, ...]"
|
|
227
306
|
),
|
|
228
307
|
)
|
|
308
|
+
source.add_argument(
|
|
309
|
+
"--counts",
|
|
310
|
+
help=(
|
|
311
|
+
"path to a skf-count-imports.py count envelope, or '-' to read from "
|
|
312
|
+
"stdin; each pair's files then carry line_a and line_b"
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
p_int.add_argument(
|
|
316
|
+
"--only",
|
|
317
|
+
metavar="NAMES",
|
|
318
|
+
help="comma-separated library names to pair; every other library is left out",
|
|
319
|
+
)
|
|
229
320
|
p_int.add_argument(
|
|
230
321
|
"--top-k",
|
|
231
322
|
type=int,
|