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
@@ -0,0 +1,504 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF Resolve Authoritative Files — §2a deterministic pre-prompt pipeline.
6
+
7
+ `skf-create-skill/references/extract.md §2a "Discovered Authoritative Files
8
+ Protocol"` performs five deterministic phases before the LLM can prompt the
9
+ user on each candidate:
10
+
11
+ 1. **Heuristic scan** — walk the source tree, find files whose case-
12
+ insensitive basename matches the auth-doc heuristic list (`llms.txt`,
13
+ `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, etc.).
14
+ 2. **Scope diff** — for each match, decide whether it is already in
15
+ §2's filtered list (matched by `scope.include` AND not by
16
+ `scope.exclude`) or excluded.
17
+ 3. **Amendment reconciliation** — consult `brief.scope.amendments[]`
18
+ for a prior decision on this path.
19
+ 4. **Preview load** — for unresolved candidates, read the first 20
20
+ lines and capture line_count + size_bytes for the prompt.
21
+ 5. **Content hash** — SHA-256 of the file's bytes (needed for any
22
+ candidate that will populate `promoted_docs[]`).
23
+
24
+ The prose previously asked the LLM to chain all five per run, with the
25
+ SHA-256 hashing and the scope-filter glob matching being the most
26
+ drift-prone parts. The helper consolidates them into one `resolve` call.
27
+
28
+ Subcommand:
29
+ resolve --source-root <path> --brief <brief.yaml-path>
30
+ [--preview-lines 20]
31
+
32
+ Output JSON (stdout):
33
+
34
+ {
35
+ "status": "no-candidates" | "candidates-found",
36
+ "summary": {
37
+ "candidates_total": N,
38
+ "already_in_scope_count": N,
39
+ "pre_decided_count": N,
40
+ "unresolved_count": N
41
+ },
42
+ "already_in_scope": [
43
+ {"path": "...", "heuristic": "llms.txt", "size_bytes": N,
44
+ "line_count": N, "content_hash": "sha256:..."}
45
+ ],
46
+ "pre_decided": [
47
+ {"path": "...", "heuristic": "...", "prior_action": "promoted"|"skipped",
48
+ "should_add_to_promoted_docs": <bool>,
49
+ "size_bytes": N|null, "line_count": N|null,
50
+ "content_hash": "sha256:..." | null}
51
+ ],
52
+ "unresolved": [
53
+ {"path": "...", "heuristic": "...", "size_bytes": N,
54
+ "line_count": N, "content_hash": "sha256:...",
55
+ "preview": "<first N lines as a string>",
56
+ "excluded_by_pattern": "<glob>" | "not matched by any scope.include"}
57
+ ]
58
+ }
59
+
60
+ Paths are emitted relative to `source-root`, forward-slash form
61
+ (cross-platform JSON convention — same as skf-detect-scripts-assets.py).
62
+
63
+ `pre_decided` semantics:
64
+ - `prior_action="promoted"` AND `should_add_to_promoted_docs=true` →
65
+ the path is in scope but the caller still needs to add it to
66
+ `promoted_docs[]` so step 5 §6 writes the `file_entries[]` row.
67
+ `content_hash` / `size_bytes` / `line_count` are populated.
68
+ - `prior_action="skipped"` → the user previously declined; caller
69
+ does nothing. Hash/size/lines are null (no need to read the file).
70
+
71
+ Exit codes:
72
+ 0 — operation succeeded (any status)
73
+ 1 — user error (paths invalid, brief unparseable)
74
+ """
75
+
76
+ from __future__ import annotations
77
+
78
+ import argparse
79
+ import hashlib
80
+ import json
81
+ import re
82
+ import sys
83
+ from pathlib import Path
84
+ from typing import Iterable
85
+
86
+ import yaml
87
+
88
+
89
+ # --------------------------------------------------------------------------
90
+ # Heuristic list — case-insensitive basename match
91
+ # --------------------------------------------------------------------------
92
+
93
+
94
+ AUTH_DOC_BASENAMES = {
95
+ "llms.txt",
96
+ "llms-full.txt",
97
+ "agents.md",
98
+ "claude.md",
99
+ "gemini.md",
100
+ "copilot.md",
101
+ ".cursorrules",
102
+ ".windsurfrules",
103
+ ".clinerules",
104
+ }
105
+
106
+
107
+ # Path-segment names that mark generated/vendored output trees — same
108
+ # exclusion set as skf-detect-scripts-assets.py.
109
+ EXCLUDED_DIR_NAMES = {
110
+ "node_modules", "__pycache__", "dist", "build", ".webpack",
111
+ "target", ".next", ".nuxt", "out", "coverage", ".git",
112
+ ".venv", "venv", ".tox", ".mypy_cache", ".pytest_cache",
113
+ ".ruff_cache", ".gradle", ".idea", ".vscode",
114
+ }
115
+
116
+
117
+ # --------------------------------------------------------------------------
118
+ # Source-tree walk
119
+ # --------------------------------------------------------------------------
120
+
121
+
122
+ def iter_auth_doc_files(source_root: Path) -> Iterable[tuple[Path, str]]:
123
+ """Yield (file_path, matched_heuristic_basename) pairs for every file
124
+ in the source tree whose basename matches an auth-doc heuristic.
125
+
126
+ Heuristic matching is case-insensitive on the basename, depth-agnostic.
127
+ Pruned: EXCLUDED_DIR_NAMES at any depth.
128
+ """
129
+ stack: list[Path] = [source_root]
130
+ while stack:
131
+ current = stack.pop()
132
+ try:
133
+ entries = list(current.iterdir())
134
+ except (PermissionError, FileNotFoundError):
135
+ continue
136
+ for entry in entries:
137
+ if entry.is_symlink():
138
+ continue
139
+ if entry.is_dir():
140
+ if entry.name in EXCLUDED_DIR_NAMES:
141
+ continue
142
+ stack.append(entry)
143
+ elif entry.is_file():
144
+ heuristic = entry.name.lower()
145
+ if heuristic in AUTH_DOC_BASENAMES:
146
+ yield entry, heuristic
147
+
148
+
149
+ # --------------------------------------------------------------------------
150
+ # Scope filtering — supports `**` recursive globs
151
+ # --------------------------------------------------------------------------
152
+
153
+
154
+ _GLOB_CACHE: dict[str, re.Pattern[str]] = {}
155
+
156
+
157
+ def _glob_to_regex(pattern: str) -> re.Pattern[str]:
158
+ """Translate a glob pattern with `**` support into a compiled regex.
159
+
160
+ Semantics (gitignore-compatible enough for brief.scope.include/exclude):
161
+ - `**` matches zero-or-more path segments (across separators)
162
+ - `*` matches anything but the path separator
163
+ - `?` matches any single character but the path separator
164
+ - other chars are escaped literally
165
+
166
+ Examples:
167
+ `**/llms.txt` → matches `llms.txt`, `a/llms.txt`, `a/b/llms.txt`
168
+ `src/**` → matches `src/anything`, `src/a/b/c`, but NOT bare `src`
169
+ `**/test_*` → matches `test_x`, `a/test_x`, `a/b/test_x`
170
+ """
171
+ cached = _GLOB_CACHE.get(pattern)
172
+ if cached is not None:
173
+ return cached
174
+
175
+ parts = pattern.split("/")
176
+ regex_parts: list[str] = []
177
+ i = 0
178
+ while i < len(parts):
179
+ part = parts[i]
180
+ if part == "**":
181
+ # `a/**/b` should match `a/b` (zero segments between) AND
182
+ # `a/x/y/b` (any number). The trick: consume the trailing `/`
183
+ # before `**` if there's one, then emit `(?:.*/)?` for the
184
+ # `**/` case or `.*` at end of pattern.
185
+ if i + 1 < len(parts):
186
+ # `**` followed by more — must allow zero-or-more segments
187
+ regex_parts.append("(?:.*/)?")
188
+ i += 1
189
+ continue
190
+ # `**` at end — match anything (including nothing)
191
+ regex_parts.append(".*")
192
+ i += 1
193
+ continue
194
+ # Translate a single segment (no `**` inside)
195
+ segment = []
196
+ for ch in part:
197
+ if ch == "*":
198
+ segment.append("[^/]*")
199
+ elif ch == "?":
200
+ segment.append("[^/]")
201
+ else:
202
+ segment.append(re.escape(ch))
203
+ regex_parts.append("".join(segment))
204
+ if i + 1 < len(parts):
205
+ regex_parts.append("/")
206
+ i += 1
207
+
208
+ regex_str = "^" + "".join(regex_parts) + "$"
209
+ compiled = re.compile(regex_str)
210
+ _GLOB_CACHE[pattern] = compiled
211
+ return compiled
212
+
213
+
214
+ def glob_match(rel_path: str, pattern: str) -> bool:
215
+ return bool(_glob_to_regex(pattern).match(rel_path))
216
+
217
+
218
+ def scope_match(
219
+ rel_path: str, includes: list[str], excludes: list[str]
220
+ ) -> tuple[bool, str | None]:
221
+ """Return (is_in_scope, excluded_by_pattern).
222
+
223
+ is_in_scope is True iff at least one include matches AND no exclude
224
+ matches. excluded_by_pattern is set when the path is OUT of scope:
225
+ - the matching exclude pattern if an exclude rule fires
226
+ - "not matched by any scope.include" if no include matches
227
+ - None when the path IS in scope
228
+ """
229
+ matched_exclude = next(
230
+ (p for p in excludes if glob_match(rel_path, p)), None
231
+ )
232
+ matched_include = any(glob_match(rel_path, p) for p in includes)
233
+
234
+ if matched_exclude is not None:
235
+ return False, matched_exclude
236
+ if not matched_include:
237
+ return False, "not matched by any scope.include"
238
+ return True, None
239
+
240
+
241
+ # --------------------------------------------------------------------------
242
+ # Brief load
243
+ # --------------------------------------------------------------------------
244
+
245
+
246
+ def load_brief(brief_path: Path) -> dict:
247
+ try:
248
+ text = brief_path.read_text(encoding="utf-8")
249
+ except OSError as exc:
250
+ raise ValueError(f"cannot read brief at {brief_path}: {exc}") from exc
251
+ try:
252
+ brief = yaml.safe_load(text)
253
+ except yaml.YAMLError as exc:
254
+ raise ValueError(
255
+ f"brief at {brief_path} is not valid YAML: {exc}"
256
+ ) from exc
257
+ if not isinstance(brief, dict):
258
+ raise ValueError(
259
+ f"brief at {brief_path} must be a YAML mapping; got "
260
+ f"{type(brief).__name__}"
261
+ )
262
+ return brief
263
+
264
+
265
+ def extract_scope(brief: dict) -> tuple[list[str], list[str], dict[str, list[str]]]:
266
+ """Extract (includes, excludes, amendments_by_path) from the brief.
267
+
268
+ amendments_by_path maps each path to a list of actions seen, in the
269
+ order they appear in the brief (so reconcile() can pick most-recent
270
+ by iterating in reverse).
271
+ """
272
+ scope = brief.get("scope") if isinstance(brief.get("scope"), dict) else {}
273
+ includes = scope.get("include") if isinstance(scope.get("include"), list) else []
274
+ excludes = scope.get("exclude") if isinstance(scope.get("exclude"), list) else []
275
+ amendments = scope.get("amendments") if isinstance(scope.get("amendments"), list) else []
276
+
277
+ by_path: dict[str, list[str]] = {}
278
+ for amend in amendments:
279
+ if not isinstance(amend, dict):
280
+ continue
281
+ path = amend.get("path")
282
+ action = amend.get("action")
283
+ if isinstance(path, str) and isinstance(action, str):
284
+ by_path.setdefault(path, []).append(action)
285
+
286
+ # filter to strings only — schema requires strings but defensively guard
287
+ includes = [p for p in includes if isinstance(p, str)]
288
+ excludes = [p for p in excludes if isinstance(p, str)]
289
+ return includes, excludes, by_path
290
+
291
+
292
+ # --------------------------------------------------------------------------
293
+ # File metadata: hash + size + lines + preview
294
+ # --------------------------------------------------------------------------
295
+
296
+
297
+ def sha256_of_file(path: Path) -> str:
298
+ h = hashlib.sha256()
299
+ with path.open("rb") as fh:
300
+ for chunk in iter(lambda: fh.read(65536), b""):
301
+ h.update(chunk)
302
+ return "sha256:" + h.hexdigest()
303
+
304
+
305
+ def count_lines(path: Path) -> int:
306
+ n = 0
307
+ try:
308
+ with path.open("rb") as fh:
309
+ for chunk in iter(lambda: fh.read(65536), b""):
310
+ n += chunk.count(b"\n")
311
+ except OSError:
312
+ return 0
313
+ return n
314
+
315
+
316
+ def load_preview(path: Path, *, max_lines: int) -> str:
317
+ """Read up to `max_lines` lines from `path` and return them as a single
318
+ string (newline-joined, no trailing newline). Best-effort UTF-8 decode;
319
+ binary content returns "".
320
+ """
321
+ try:
322
+ with path.open("r", encoding="utf-8", errors="replace") as fh:
323
+ lines: list[str] = []
324
+ for _ in range(max_lines):
325
+ line = fh.readline()
326
+ if not line:
327
+ break
328
+ lines.append(line.rstrip("\n"))
329
+ return "\n".join(lines)
330
+ except OSError:
331
+ return ""
332
+
333
+
334
+ # --------------------------------------------------------------------------
335
+ # Resolve
336
+ # --------------------------------------------------------------------------
337
+
338
+
339
+ _PROMOTED = "promoted"
340
+ _SKIPPED = "skipped"
341
+
342
+
343
+ def _latest_action(actions: list[str]) -> str | None:
344
+ """Most-recent matching amendment wins. Returns 'promoted', 'skipped',
345
+ or None if no recognized action was seen."""
346
+ for action in reversed(actions):
347
+ if action in (_PROMOTED, _SKIPPED):
348
+ return action
349
+ return None
350
+
351
+
352
+ def resolve(
353
+ source_root: Path,
354
+ brief_path: Path,
355
+ *,
356
+ preview_lines: int = 20,
357
+ ) -> dict:
358
+ """Run the §2a deterministic pipeline. See module docstring for output shape."""
359
+ brief = load_brief(brief_path)
360
+ includes, excludes, amendments_by_path = extract_scope(brief)
361
+
362
+ already_in_scope: list[dict] = []
363
+ pre_decided: list[dict] = []
364
+ unresolved: list[dict] = []
365
+
366
+ for file_path, heuristic in iter_auth_doc_files(source_root):
367
+ rel = file_path.relative_to(source_root).as_posix()
368
+ in_scope, excluded_by = scope_match(rel, includes, excludes)
369
+ prior_action = _latest_action(amendments_by_path.get(rel, []))
370
+
371
+ if in_scope and prior_action != _SKIPPED:
372
+ # Already in scope — remove from §2 filtered list and add to
373
+ # promoted_docs[]. Includes the "user manually added to
374
+ # scope.include" case AND the "amendments has promoted" case
375
+ # (which is the deterministic replay path).
376
+ already_in_scope.append({
377
+ "path": rel,
378
+ "heuristic": heuristic,
379
+ "size_bytes": file_path.stat().st_size,
380
+ "line_count": count_lines(file_path),
381
+ "content_hash": sha256_of_file(file_path),
382
+ })
383
+ continue
384
+
385
+ if prior_action == _PROMOTED:
386
+ # Amendment says promoted but scope.include doesn't currently
387
+ # match — unusual (brief was edited?) — still populate
388
+ # promoted_docs[] per the deterministic replay rule.
389
+ pre_decided.append({
390
+ "path": rel,
391
+ "heuristic": heuristic,
392
+ "prior_action": _PROMOTED,
393
+ "should_add_to_promoted_docs": True,
394
+ "size_bytes": file_path.stat().st_size,
395
+ "line_count": count_lines(file_path),
396
+ "content_hash": sha256_of_file(file_path),
397
+ })
398
+ continue
399
+
400
+ if prior_action == _SKIPPED:
401
+ pre_decided.append({
402
+ "path": rel,
403
+ "heuristic": heuristic,
404
+ "prior_action": _SKIPPED,
405
+ "should_add_to_promoted_docs": False,
406
+ "size_bytes": None,
407
+ "line_count": None,
408
+ "content_hash": None,
409
+ })
410
+ continue
411
+
412
+ # No prior amendment and out of scope → unresolved
413
+ unresolved.append({
414
+ "path": rel,
415
+ "heuristic": heuristic,
416
+ "size_bytes": file_path.stat().st_size,
417
+ "line_count": count_lines(file_path),
418
+ "content_hash": sha256_of_file(file_path),
419
+ "preview": load_preview(file_path, max_lines=preview_lines),
420
+ "excluded_by_pattern": excluded_by,
421
+ })
422
+
423
+ # Deterministic ordering for stable diffs / cache keys
424
+ already_in_scope.sort(key=lambda r: r["path"])
425
+ pre_decided.sort(key=lambda r: r["path"])
426
+ unresolved.sort(key=lambda r: r["path"])
427
+
428
+ candidates_total = (
429
+ len(already_in_scope) + len(pre_decided) + len(unresolved)
430
+ )
431
+ return {
432
+ "status": "candidates-found" if candidates_total > 0 else "no-candidates",
433
+ "summary": {
434
+ "candidates_total": candidates_total,
435
+ "already_in_scope_count": len(already_in_scope),
436
+ "pre_decided_count": len(pre_decided),
437
+ "unresolved_count": len(unresolved),
438
+ },
439
+ "already_in_scope": already_in_scope,
440
+ "pre_decided": pre_decided,
441
+ "unresolved": unresolved,
442
+ }
443
+
444
+
445
+ # --------------------------------------------------------------------------
446
+ # CLI
447
+ # --------------------------------------------------------------------------
448
+
449
+
450
+ def _cmd_resolve(args: argparse.Namespace) -> int:
451
+ source_root = Path(args.source_root)
452
+ if not source_root.is_dir():
453
+ print(
454
+ f"error: source-root not a directory: {source_root}", file=sys.stderr
455
+ )
456
+ return 1
457
+ brief_path = Path(args.brief)
458
+ if not brief_path.is_file():
459
+ print(f"error: brief not found: {brief_path}", file=sys.stderr)
460
+ return 1
461
+ if args.preview_lines < 1:
462
+ print("error: --preview-lines must be >= 1", file=sys.stderr)
463
+ return 1
464
+ try:
465
+ result = resolve(source_root, brief_path, preview_lines=args.preview_lines)
466
+ except ValueError as exc:
467
+ print(f"error: {exc}", file=sys.stderr)
468
+ return 1
469
+ json.dump(result, sys.stdout, indent=2)
470
+ sys.stdout.write("\n")
471
+ return 0
472
+
473
+
474
+ def _build_parser() -> argparse.ArgumentParser:
475
+ parser = argparse.ArgumentParser(
476
+ prog="skf-resolve-authoritative-files",
477
+ description=(
478
+ "Scan a source tree for authoritative AI documentation files, "
479
+ "classify each against scope filters + amendments, emit "
480
+ "already-in-scope / pre-decided / unresolved buckets."
481
+ ),
482
+ )
483
+ sub = parser.add_subparsers(dest="cmd", required=True)
484
+ p = sub.add_parser("resolve", help="run the §2a pipeline")
485
+ p.add_argument("--source-root", required=True, help="path to the source tree")
486
+ p.add_argument("--brief", required=True, help="path to skill-brief.yaml")
487
+ p.add_argument(
488
+ "--preview-lines",
489
+ type=int,
490
+ default=20,
491
+ help="number of lines to capture for the prompt preview (default: 20)",
492
+ )
493
+ p.set_defaults(func=_cmd_resolve)
494
+ return parser
495
+
496
+
497
+ def main(argv: list[str] | None = None) -> int:
498
+ parser = _build_parser()
499
+ args = parser.parse_args(argv)
500
+ return args.func(args)
501
+
502
+
503
+ if __name__ == "__main__":
504
+ raise SystemExit(main())