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.
Files changed (230) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +0 -8
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/docs/_internal/STABILITY.md +1 -1
  5. package/docs/architecture.md +2 -2
  6. package/docs/examples.md +1 -1
  7. package/docs/workflows.md +5 -5
  8. package/package.json +2 -2
  9. package/src/README.md +1 -1
  10. package/src/knowledge/ccc-bridge.md +12 -12
  11. package/src/knowledge/qmd-registry.md +5 -5
  12. package/src/shared/health-check.md +5 -5
  13. package/src/shared/references/description-guard-protocol.md +100 -0
  14. package/src/shared/references/output-contract-schema.md +1 -1
  15. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
  16. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
  17. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
  18. package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
  19. package/src/shared/scripts/skf-build-change-manifest.py +420 -0
  20. package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
  21. package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
  22. package/src/shared/scripts/skf-description-guard.py +359 -0
  23. package/src/shared/scripts/skf-detect-language.py +2 -2
  24. package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
  25. package/src/shared/scripts/skf-detect-tools.py +97 -8
  26. package/src/shared/scripts/skf-detect-workspaces.py +1 -1
  27. package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
  29. package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
  30. package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
  31. package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
  32. package/src/shared/scripts/skf-hash-content.py +284 -0
  33. package/src/shared/scripts/skf-load-provenance.py +295 -0
  34. package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
  35. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  36. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  37. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  38. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  39. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  40. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  41. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  42. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  43. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  44. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  45. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  46. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  47. package/src/skf-analyze-source/SKILL.md +69 -16
  48. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  49. package/src/skf-analyze-source/customize.toml +55 -0
  50. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  51. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  52. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  53. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +44 -14
  54. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +27 -6
  55. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  56. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  57. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +12 -10
  58. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  59. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  60. package/src/skf-audit-skill/SKILL.md +66 -15
  61. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  62. package/src/skf-audit-skill/customize.toml +49 -0
  63. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  64. package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +57 -19
  65. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  66. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  67. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  68. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  69. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  70. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +29 -17
  71. package/src/skf-brief-skill/SKILL.md +48 -18
  72. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  73. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  74. package/src/skf-brief-skill/customize.toml +44 -0
  75. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +5 -5
  76. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  77. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  78. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +24 -24
  79. package/src/skf-brief-skill/references/headless-args.md +9 -9
  80. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  81. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  82. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  83. package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
  84. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +19 -16
  85. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  86. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +11 -10
  87. package/src/skf-create-skill/SKILL.md +25 -19
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  89. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  90. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  91. package/src/skf-create-skill/customize.toml +35 -0
  92. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  93. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  94. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  96. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  97. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  98. package/src/skf-create-skill/references/extraction-patterns.md +8 -8
  99. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
  100. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  101. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  102. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  103. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  104. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  105. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  106. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  107. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +46 -26
  108. package/src/skf-create-stack-skill/SKILL.md +76 -17
  109. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  110. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  111. package/src/skf-create-stack-skill/customize.toml +46 -0
  112. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  113. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  114. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +32 -11
  115. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +28 -28
  116. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  117. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  118. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  119. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  120. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +40 -17
  122. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  123. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  124. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  125. package/src/skf-drop-skill/SKILL.md +84 -12
  126. package/src/skf-drop-skill/customize.toml +57 -0
  127. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  128. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  129. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  130. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  131. package/src/skf-export-skill/SKILL.md +85 -16
  132. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  133. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  134. package/src/skf-export-skill/customize.toml +49 -0
  135. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
  136. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  137. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
  138. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  139. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  140. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  141. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  142. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  143. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +74 -0
  144. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +20 -8
  145. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  146. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
  147. package/src/skf-quick-skill/SKILL.md +63 -123
  148. package/src/skf-quick-skill/customize.toml +44 -0
  149. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  150. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  151. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  152. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  153. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  154. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  155. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  156. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  157. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  158. package/src/skf-refine-architecture/SKILL.md +86 -16
  159. package/src/skf-refine-architecture/customize.toml +49 -0
  160. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  161. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  162. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  163. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  164. package/src/skf-refine-architecture/references/init.md +144 -0
  165. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  166. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  167. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  168. package/src/skf-rename-skill/SKILL.md +82 -12
  169. package/src/skf-rename-skill/customize.toml +52 -0
  170. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  171. package/src/skf-rename-skill/references/health-check.md +30 -0
  172. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  173. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  174. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  175. package/src/skf-setup/SKILL.md +35 -35
  176. package/src/skf-setup/customize.toml +33 -0
  177. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  178. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
  179. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  180. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  181. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  182. package/src/skf-setup/references/tier-rules.md +1 -1
  183. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  184. package/src/skf-test-skill/SKILL.md +77 -15
  185. package/src/skf-test-skill/customize.toml +54 -0
  186. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +28 -71
  187. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  188. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  189. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  190. package/src/skf-test-skill/references/health-check.md +14 -0
  191. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  192. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  193. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  194. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  195. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  196. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  197. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  198. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  199. package/src/skf-update-skill/SKILL.md +23 -14
  200. package/src/skf-update-skill/customize.toml +44 -0
  201. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  202. package/src/skf-update-skill/references/health-check.md +32 -0
  203. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  204. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  205. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  206. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  207. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  208. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  209. package/src/skf-update-skill/references/report.md +214 -0
  210. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  211. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  212. package/src/skf-verify-stack/SKILL.md +89 -17
  213. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  214. package/src/skf-verify-stack/customize.toml +50 -0
  215. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  216. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +7 -7
  217. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  218. package/src/skf-verify-stack/references/init.md +166 -0
  219. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  220. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +12 -12
  221. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +22 -41
  222. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +7 -7
  223. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +8 -8
  224. package/tools/validate-docs-drift.js +1 -1
  225. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  226. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  227. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  228. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  229. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  230. 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
- steps-c/step-01b-ccc-index.md` §2b with one Python invocation. Reads
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 file is supposed to "
176
- f"substitute {{project-root}} and reduce to a basename before "
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())