bmad-module-skill-forge 1.4.0 → 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 (231) 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 +45 -1
  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/references/init.md +305 -0
  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 -16
  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} +17 -9
  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} +18 -12
  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} +21 -51
  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} +30 -6
  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} +116 -91
  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} +14 -22
  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-audit-skill/steps-c/step-01-init.md +0 -221
  226. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  227. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  228. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  229. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  230. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  231. 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
@@ -33,6 +33,11 @@ Validation rules for the 2 conditional values (PR #248):
33
33
  → produces `**//abs/path`, `**/~/x`, or `**/./rel` — malformed glob
34
34
  - REJECT values containing glob meta-characters (`*`, `?`, `[`)
35
35
  → interpolation collides with the surrounding pattern syntax
36
+ - REJECT values containing unresolved `{project-root}`-style placeholders
37
+ (any `{` or `}` character) — produces `**/{project-root}/x`, which
38
+ cocoindex accepts as a literal-segment glob and silently never matches.
39
+ This is the bug from issue #293: the step file was passing template
40
+ strings straight through.
36
41
 
37
42
  Rejected values are SKIPPED (the pattern is not added) and a warning
38
43
  is appended to the output. The 4 always-include patterns are applied
@@ -96,6 +101,7 @@ ALWAYS_INCLUDE = (
96
101
  "**/_skf-learn",
97
102
  )
98
103
  GLOB_META_CHARS = set("*?[")
104
+ PLACEHOLDER_CHARS = set("{}")
99
105
 
100
106
 
101
107
  def _die(code: int, message: str) -> None:
@@ -133,6 +139,29 @@ def _atomic_write(target: Path, content: str) -> None:
133
139
  # ─── Config-value validation (PR #248 rules) ────────────────────────────────
134
140
 
135
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
+
136
165
  def validate_config_value(key: str, raw_value: str) -> tuple[str | None, str | None]:
137
166
  """Validate a config value used to interpolate `**/{value}`.
138
167
 
@@ -163,6 +192,13 @@ def validate_config_value(key: str, raw_value: str) -> tuple[str | None, str | N
163
192
  f"pattern syntax — fix the value in {{project-root}}/_bmad/skf/config.yaml"
164
193
  )
165
194
 
195
+ if any(ch in PLACEHOLDER_CHARS for ch in value):
196
+ return None, (
197
+ f"{key} contains an unresolved template placeholder ({{ or }}); "
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}}"
200
+ )
201
+
166
202
  return value, None
167
203
 
168
204
 
@@ -252,6 +288,12 @@ def render_settings_yml(data: dict, original_text: str | None) -> str:
252
288
  def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder: str) -> None:
253
289
  target = project_root / ".cocoindex_code" / "settings.yml"
254
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
+
255
297
  patterns_to_add, warnings = assemble_patterns(skills_output_folder, forge_data_folder)
256
298
  data, existed = read_settings_yml(target)
257
299
 
@@ -274,6 +316,7 @@ def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder:
274
316
  "patterns_added": 0,
275
317
  "patterns_added_list": [],
276
318
  "patterns_already_present": patterns_already_present,
319
+ "effective_patterns": sorted(set(patterns_to_add)),
277
320
  "written": False,
278
321
  "warnings": warnings,
279
322
  })
@@ -289,6 +332,7 @@ def cmd_merge(project_root: Path, skills_output_folder: str, forge_data_folder:
289
332
  "patterns_added": len(newly_added),
290
333
  "patterns_added_list": newly_added,
291
334
  "patterns_already_present": patterns_already_present,
335
+ "effective_patterns": sorted(set(patterns_to_add)),
292
336
  "written": True,
293
337
  "warnings": warnings,
294
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())