bmad-module-skill-forge 1.4.1 → 1.5.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 +0 -8
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/STABILITY.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/examples.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +2 -2
- package/src/README.md +1 -1
- package/src/knowledge/ccc-bridge.md +12 -12
- package/src/knowledge/qmd-registry.md +5 -5
- package/src/shared/health-check.md +5 -5
- package/src/shared/references/description-guard-protocol.md +100 -0
- package/src/shared/references/output-contract-schema.md +1 -1
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
- package/src/shared/scripts/skf-build-change-manifest.py +420 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
- package/src/shared/scripts/skf-description-guard.py +359 -0
- package/src/shared/scripts/skf-detect-language.py +2 -2
- package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
- package/src/shared/scripts/skf-detect-tools.py +97 -8
- package/src/shared/scripts/skf-detect-workspaces.py +1 -1
- package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
- package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
- package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
- package/src/shared/scripts/skf-hash-content.py +284 -0
- package/src/shared/scripts/skf-load-provenance.py +295 -0
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
- package/src/shared/scripts/skf-pair-intersect.py +250 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
- package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
- package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
- package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
- package/src/shared/scripts/skf-scan-manifests.py +738 -0
- package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
- package/src/shared/scripts/skf-update-active-symlink.py +315 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
- package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
- package/src/shared/scripts/skf-write-skill-brief.py +61 -5
- package/src/skf-analyze-source/SKILL.md +69 -16
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +55 -0
- package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
- package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
- package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +44 -14
- package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +27 -6
- package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
- package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
- package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +12 -10
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
- package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
- package/src/skf-audit-skill/SKILL.md +66 -15
- package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
- package/src/skf-audit-skill/customize.toml +49 -0
- package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +57 -19
- package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
- package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
- package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
- package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
- package/src/skf-audit-skill/references/severity-rules.md +2 -0
- package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +29 -17
- package/src/skf-brief-skill/SKILL.md +48 -18
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
- package/src/skf-brief-skill/customize.toml +44 -0
- package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +5 -5
- package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
- package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
- package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +24 -24
- package/src/skf-brief-skill/references/headless-args.md +9 -9
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
- package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
- package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
- package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +19 -16
- package/src/skf-brief-skill/references/version-resolution.md +2 -2
- package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +11 -10
- package/src/skf-create-skill/SKILL.md +25 -19
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
- package/src/skf-create-skill/assets/skill-sections.md +5 -5
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
- package/src/skf-create-skill/customize.toml +35 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
- package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
- package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
- package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
- package/src/skf-create-skill/references/extraction-patterns.md +8 -8
- package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
- package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
- package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
- package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
- package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
- package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
- package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
- package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +46 -26
- package/src/skf-create-stack-skill/SKILL.md +76 -17
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +46 -0
- package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
- package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +32 -11
- package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +28 -28
- package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
- package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
- package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
- package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +40 -17
- package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
- package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
- package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
- package/src/skf-drop-skill/SKILL.md +84 -12
- package/src/skf-drop-skill/customize.toml +57 -0
- package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
- package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
- package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
- package/src/skf-export-skill/SKILL.md +85 -16
- package/src/skf-export-skill/assets/managed-section-format.md +3 -3
- package/src/skf-export-skill/assets/snippet-format.md +2 -2
- package/src/skf-export-skill/customize.toml +49 -0
- package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
- package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
- package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
- package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
- package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
- package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
- package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +74 -0
- package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +20 -8
- package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
- package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
- package/src/skf-quick-skill/SKILL.md +63 -123
- package/src/skf-quick-skill/customize.toml +44 -0
- package/src/skf-quick-skill/references/batch-mode.md +102 -0
- package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
- package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
- package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
- package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
- package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
- package/src/skf-quick-skill/references/registry-resolution.md +2 -0
- package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
- package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
- package/src/skf-refine-architecture/SKILL.md +86 -16
- package/src/skf-refine-architecture/customize.toml +49 -0
- package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
- package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
- package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
- package/src/skf-refine-architecture/references/init.md +144 -0
- package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
- package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
- package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
- package/src/skf-rename-skill/SKILL.md +82 -12
- package/src/skf-rename-skill/customize.toml +52 -0
- package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
- package/src/skf-rename-skill/references/health-check.md +30 -0
- package/src/skf-rename-skill/references/rebuild-context.md +110 -0
- package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
- package/src/skf-setup/SKILL.md +35 -35
- package/src/skf-setup/customize.toml +33 -0
- package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
- package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
- package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
- package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
- package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
- package/src/skf-test-skill/SKILL.md +77 -15
- package/src/skf-test-skill/customize.toml +54 -0
- package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +28 -71
- package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
- package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
- package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
- package/src/skf-test-skill/references/health-check.md +14 -0
- package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
- package/src/skf-test-skill/references/migration-section-rules.md +4 -2
- package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
- package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +7 -5
- package/src/skf-test-skill/scripts/compute-score.py +88 -17
- package/src/skf-test-skill/templates/test-report-template.md +13 -13
- package/src/skf-update-skill/SKILL.md +23 -14
- package/src/skf-update-skill/customize.toml +44 -0
- package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
- package/src/skf-update-skill/references/health-check.md +32 -0
- package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
- package/src/skf-update-skill/references/manual-section-rules.md +4 -0
- package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
- package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
- package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
- package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
- package/src/skf-update-skill/references/report.md +214 -0
- package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
- package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
- package/src/skf-verify-stack/SKILL.md +89 -17
- package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
- package/src/skf-verify-stack/customize.toml +50 -0
- package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
- package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +7 -7
- package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
- package/src/skf-verify-stack/references/init.md +166 -0
- package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
- package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +12 -12
- package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +22 -41
- package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +7 -7
- package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +8 -8
- package/tools/validate-docs-drift.js +1 -1
- package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
- package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
- package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
- package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
- package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"""SKF Merge CCC Exclusions — Set-union merge of SKF exclusion patterns.
|
|
6
6
|
|
|
7
7
|
Replaces the prose-driven validation + merge logic in `src/skf-setup/
|
|
8
|
-
|
|
8
|
+
references/ccc-index.md` §2b with one Python invocation. Reads
|
|
9
9
|
`.cocoindex_code/settings.yml`, applies PR #248's config-value
|
|
10
10
|
validation rules to reject inputs that would silently produce a
|
|
11
11
|
malformed glob (the empty-value-becomes-`**/`-matches-everything bug
|
|
@@ -139,6 +139,29 @@ def _atomic_write(target: Path, content: str) -> None:
|
|
|
139
139
|
# ─── Config-value validation (PR #248 rules) ────────────────────────────────
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def resolve_repo_relative(raw_value: str, project_root: Path) -> str:
|
|
143
|
+
"""Normalize a config value like '{project-root}/skills' to 'skills'.
|
|
144
|
+
|
|
145
|
+
Accepts:
|
|
146
|
+
- `{project-root}/<segments>` → returns `<segments>` (verbatim, no globs)
|
|
147
|
+
- `<segments>` (already relative) → returns as-is, stripped
|
|
148
|
+
- Other input → returns the stripped value (validate_config_value
|
|
149
|
+
will catch absolute paths, placeholders, glob-meta).
|
|
150
|
+
|
|
151
|
+
Owning the substitution here removes ~150 tokens of "substitute and
|
|
152
|
+
basename-or-keep-tail" reasoning from every step prompt that invokes
|
|
153
|
+
this helper.
|
|
154
|
+
"""
|
|
155
|
+
if raw_value is None:
|
|
156
|
+
return ""
|
|
157
|
+
value = str(raw_value).strip()
|
|
158
|
+
if value.startswith("{project-root}/"):
|
|
159
|
+
value = value[len("{project-root}/"):]
|
|
160
|
+
elif value == "{project-root}":
|
|
161
|
+
value = ""
|
|
162
|
+
return value
|
|
163
|
+
|
|
164
|
+
|
|
142
165
|
def validate_config_value(key: str, raw_value: str) -> tuple[str | None, str | None]:
|
|
143
166
|
"""Validate a config value used to interpolate `**/{value}`.
|
|
144
167
|
|
|
@@ -172,9 +195,8 @@ def validate_config_value(key: str, raw_value: str) -> tuple[str | None, str | N
|
|
|
172
195
|
if any(ch in PLACEHOLDER_CHARS for ch in value):
|
|
173
196
|
return None, (
|
|
174
197
|
f"{key} contains an unresolved template placeholder ({{ or }}); "
|
|
175
|
-
f"refused for ccc exclusion because the step
|
|
176
|
-
f"
|
|
177
|
-
f"invoking the helper — see step-01b §3 in src/skf-setup/steps-c/"
|
|
198
|
+
f"refused for ccc exclusion because the step is supposed to forward "
|
|
199
|
+
f"the raw config value and let this script resolve {{project-root}}"
|
|
178
200
|
)
|
|
179
201
|
|
|
180
202
|
return value, None
|
|
@@ -266,6 +288,12 @@ def render_settings_yml(data: dict, original_text: str | None) -> str:
|
|
|
266
288
|
def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder: str) -> None:
|
|
267
289
|
target = project_root / ".cocoindex_code" / "settings.yml"
|
|
268
290
|
|
|
291
|
+
# Resolve `{project-root}/...` template strings here so callers can
|
|
292
|
+
# forward raw config values verbatim. This removes the "substitute and
|
|
293
|
+
# basename" reasoning from step prose — the script owns the contract.
|
|
294
|
+
skills_output_folder = resolve_repo_relative(skills_output_folder, project_root)
|
|
295
|
+
forge_data_folder = resolve_repo_relative(forge_data_folder, project_root)
|
|
296
|
+
|
|
269
297
|
patterns_to_add, warnings = assemble_patterns(skills_output_folder, forge_data_folder)
|
|
270
298
|
data, existed = read_settings_yml(target)
|
|
271
299
|
|
|
@@ -288,6 +316,7 @@ def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder:
|
|
|
288
316
|
"patterns_added": 0,
|
|
289
317
|
"patterns_added_list": [],
|
|
290
318
|
"patterns_already_present": patterns_already_present,
|
|
319
|
+
"effective_patterns": sorted(set(patterns_to_add)),
|
|
291
320
|
"written": False,
|
|
292
321
|
"warnings": warnings,
|
|
293
322
|
})
|
|
@@ -303,6 +332,7 @@ def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder:
|
|
|
303
332
|
"patterns_added": len(newly_added),
|
|
304
333
|
"patterns_added_list": newly_added,
|
|
305
334
|
"patterns_already_present": patterns_already_present,
|
|
335
|
+
"effective_patterns": sorted(set(patterns_to_add)),
|
|
306
336
|
"written": True,
|
|
307
337
|
"warnings": warnings,
|
|
308
338
|
})
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Pair Intersect — compute file-list intersections between library pairs.
|
|
6
|
+
|
|
7
|
+
`detect-integrations.md` §1 ("File-list intersection fast path") asks the LLM
|
|
8
|
+
to prune the N*(N-1)/2 pair-grep matrix by intersecting the per-library file
|
|
9
|
+
lists that step 3 import-count extraction already produced. At N≈21 this
|
|
10
|
+
collapses ~210 prescribed pair greps to a handful of non-empty-intersection
|
|
11
|
+
pairs in typical codebases; the LLM only needs to run further grep passes
|
|
12
|
+
against those.
|
|
13
|
+
|
|
14
|
+
This script extracts that intersection from prose-orchestrated work to a
|
|
15
|
+
deterministic helper. The LLM still composes the user-facing warning when
|
|
16
|
+
the Top-K cap kicks in — the script just flags `truncated: true` so the
|
|
17
|
+
caller knows there's something to warn about.
|
|
18
|
+
|
|
19
|
+
Subcommand:
|
|
20
|
+
intersect --libraries <json-file-or-'-'> [--top-k N]
|
|
21
|
+
Read a libraries JSON (either from <json-file> or stdin if '-') of the
|
|
22
|
+
shape:
|
|
23
|
+
[{"name": "<lib>", "files": ["<path>", ...]}, ...]
|
|
24
|
+
where each library entry lists the project files that import it (the
|
|
25
|
+
per-library file enumeration from detect-integrations.md step 3).
|
|
26
|
+
|
|
27
|
+
Emit JSON:
|
|
28
|
+
{
|
|
29
|
+
"pairs": [
|
|
30
|
+
{"a": "<lib>", "b": "<lib>", "intersection_count": N,
|
|
31
|
+
"files": ["<rel-path-forward-slash>", ...]},
|
|
32
|
+
...
|
|
33
|
+
],
|
|
34
|
+
"truncated": false,
|
|
35
|
+
"total_pairs": <full non-empty-intersection count before cap>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Pairs are computed for each unordered pair (a, b) with a < b
|
|
39
|
+
lexicographically; only non-empty intersections are emitted. The pairs
|
|
40
|
+
list is sorted by intersection_count DESC, then by (a, b) ASC for
|
|
41
|
+
stable, reproducible ordering.
|
|
42
|
+
|
|
43
|
+
Top-K cap: default --top-k 20 (matches detect-integrations.md §1's
|
|
44
|
+
"Top-K cap at 20 with explicit warning"). If the full count exceeds
|
|
45
|
+
the cap, output is truncated to top --top-k pairs and `truncated`
|
|
46
|
+
becomes true.
|
|
47
|
+
|
|
48
|
+
CLI examples:
|
|
49
|
+
uv run skf-pair-intersect.py intersect --libraries libs.json
|
|
50
|
+
cat libs.json | uv run skf-pair-intersect.py intersect --libraries -
|
|
51
|
+
uv run skf-pair-intersect.py intersect --libraries libs.json --top-k 50
|
|
52
|
+
|
|
53
|
+
Exit codes:
|
|
54
|
+
0 — operation succeeded (including: empty input → empty pairs list)
|
|
55
|
+
1 — user error (bad JSON, missing required `name`/`files` field on any
|
|
56
|
+
library entry, file not readable)
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import argparse
|
|
62
|
+
import json
|
|
63
|
+
import sys
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
DEFAULT_TOP_K = 20
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
# Pair intersection
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_libraries(libraries: object) -> list[dict]:
|
|
76
|
+
"""Validate the libraries input is a list of {name, files} records.
|
|
77
|
+
|
|
78
|
+
Returns the list with each entry's `files` normalized to a list of
|
|
79
|
+
forward-slash strings. Raises ValueError on any structural defect.
|
|
80
|
+
"""
|
|
81
|
+
if not isinstance(libraries, list):
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"libraries input must be a JSON array; got {type(libraries).__name__}"
|
|
84
|
+
)
|
|
85
|
+
normalized: list[dict] = []
|
|
86
|
+
for idx, entry in enumerate(libraries):
|
|
87
|
+
if not isinstance(entry, dict):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"library entry at index {idx} is not an object: {entry!r}"
|
|
90
|
+
)
|
|
91
|
+
name = entry.get("name")
|
|
92
|
+
if not isinstance(name, str) or not name:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"library entry at index {idx} missing required `name` field"
|
|
95
|
+
)
|
|
96
|
+
files = entry.get("files")
|
|
97
|
+
if not isinstance(files, list):
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"library entry {name!r} missing required `files` array"
|
|
100
|
+
)
|
|
101
|
+
norm_files: list[str] = []
|
|
102
|
+
for f_idx, f in enumerate(files):
|
|
103
|
+
if not isinstance(f, str):
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"library {name!r} files[{f_idx}] is not a string: {f!r}"
|
|
106
|
+
)
|
|
107
|
+
# normalize separators to forward slash for cross-platform
|
|
108
|
+
# JSON output; preserves caller-supplied relative paths.
|
|
109
|
+
norm_files.append(f.replace("\\", "/"))
|
|
110
|
+
normalized.append({"name": name, "files": norm_files})
|
|
111
|
+
return normalized
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compute_pairs(libraries: list[dict]) -> list[dict]:
|
|
115
|
+
"""For each unordered (a, b) with a < b lexicographically, compute the
|
|
116
|
+
file-list intersection. Emit only pairs with non-empty intersections.
|
|
117
|
+
|
|
118
|
+
The returned list is sorted by intersection_count DESC, then (a, b) ASC.
|
|
119
|
+
"""
|
|
120
|
+
# sort libraries by name for stable iteration order
|
|
121
|
+
sorted_libs = sorted(libraries, key=lambda lib: lib["name"])
|
|
122
|
+
pairs: list[dict] = []
|
|
123
|
+
n = len(sorted_libs)
|
|
124
|
+
for i in range(n):
|
|
125
|
+
for j in range(i + 1, n):
|
|
126
|
+
a = sorted_libs[i]
|
|
127
|
+
b = sorted_libs[j]
|
|
128
|
+
# by sort order, a["name"] <= b["name"]; if equal we skip
|
|
129
|
+
# (duplicate library names are silently collapsed into one
|
|
130
|
+
# pair-with-itself, which is not an integration candidate).
|
|
131
|
+
if a["name"] == b["name"]:
|
|
132
|
+
continue
|
|
133
|
+
set_a = set(a["files"])
|
|
134
|
+
set_b = set(b["files"])
|
|
135
|
+
intersection = sorted(set_a & set_b)
|
|
136
|
+
if not intersection:
|
|
137
|
+
continue
|
|
138
|
+
pairs.append({
|
|
139
|
+
"a": a["name"],
|
|
140
|
+
"b": b["name"],
|
|
141
|
+
"intersection_count": len(intersection),
|
|
142
|
+
"files": intersection,
|
|
143
|
+
})
|
|
144
|
+
pairs.sort(key=lambda p: (-p["intersection_count"], p["a"], p["b"]))
|
|
145
|
+
return pairs
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def intersect(libraries: list[dict], top_k: int = DEFAULT_TOP_K) -> dict:
|
|
149
|
+
"""Compute pairs and apply Top-K cap."""
|
|
150
|
+
all_pairs = compute_pairs(libraries)
|
|
151
|
+
total = len(all_pairs)
|
|
152
|
+
if top_k is not None and top_k >= 0 and total > top_k:
|
|
153
|
+
return {
|
|
154
|
+
"pairs": all_pairs[:top_k],
|
|
155
|
+
"truncated": True,
|
|
156
|
+
"total_pairs": total,
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
"pairs": all_pairs,
|
|
160
|
+
"truncated": False,
|
|
161
|
+
"total_pairs": total,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------------
|
|
166
|
+
# CLI
|
|
167
|
+
# --------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _read_libraries_source(source: str) -> object:
|
|
171
|
+
"""Read libraries JSON from a file path or stdin (if source == '-')."""
|
|
172
|
+
if source == "-":
|
|
173
|
+
try:
|
|
174
|
+
text = sys.stdin.read()
|
|
175
|
+
except OSError as exc:
|
|
176
|
+
raise ValueError(f"failed to read libraries JSON from stdin: {exc}") from exc
|
|
177
|
+
else:
|
|
178
|
+
path = Path(source)
|
|
179
|
+
if not path.is_file():
|
|
180
|
+
raise ValueError(f"libraries file not found: {path}")
|
|
181
|
+
try:
|
|
182
|
+
text = path.read_text(encoding="utf-8")
|
|
183
|
+
except OSError as exc:
|
|
184
|
+
raise ValueError(f"failed to read libraries file {path}: {exc}") from exc
|
|
185
|
+
try:
|
|
186
|
+
return json.loads(text)
|
|
187
|
+
except json.JSONDecodeError as exc:
|
|
188
|
+
raise ValueError(f"malformed JSON in libraries input: {exc}") from exc
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _cmd_intersect(args: argparse.Namespace) -> int:
|
|
192
|
+
try:
|
|
193
|
+
raw = _read_libraries_source(args.libraries)
|
|
194
|
+
libraries = validate_libraries(raw)
|
|
195
|
+
except ValueError as exc:
|
|
196
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
if args.top_k < 0:
|
|
199
|
+
print(f"error: --top-k must be >= 0; got {args.top_k}", file=sys.stderr)
|
|
200
|
+
return 1
|
|
201
|
+
result = intersect(libraries, top_k=args.top_k)
|
|
202
|
+
json.dump(result, sys.stdout, indent=2)
|
|
203
|
+
sys.stdout.write("\n")
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
208
|
+
parser = argparse.ArgumentParser(
|
|
209
|
+
prog="skf-pair-intersect",
|
|
210
|
+
description=(
|
|
211
|
+
"Compute file-list intersections between library pairs "
|
|
212
|
+
"(detect-integrations.md §1 fast path)."
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
216
|
+
|
|
217
|
+
p_int = sub.add_parser(
|
|
218
|
+
"intersect",
|
|
219
|
+
help="emit non-empty-intersection pairs as JSON, sorted DESC by count",
|
|
220
|
+
)
|
|
221
|
+
p_int.add_argument(
|
|
222
|
+
"--libraries",
|
|
223
|
+
required=True,
|
|
224
|
+
help=(
|
|
225
|
+
"path to libraries JSON, or '-' to read from stdin. "
|
|
226
|
+
"Shape: [{\"name\": \"<lib>\", \"files\": [\"<path>\", ...]}, ...]"
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
p_int.add_argument(
|
|
230
|
+
"--top-k",
|
|
231
|
+
type=int,
|
|
232
|
+
default=DEFAULT_TOP_K,
|
|
233
|
+
help=(
|
|
234
|
+
f"cap output at top N pairs by intersection_count (default {DEFAULT_TOP_K}); "
|
|
235
|
+
"set 0 to allow only zero pairs (effectively a 'count-only' mode)"
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
p_int.set_defaults(func=_cmd_intersect)
|
|
239
|
+
|
|
240
|
+
return parser
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main(argv: list[str] | None = None) -> int:
|
|
244
|
+
parser = _build_parser()
|
|
245
|
+
args = parser.parse_args(argv)
|
|
246
|
+
return args.func(args)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
raise SystemExit(main())
|