bmad-module-skill-forge 1.4.1 → 1.5.1

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 +147 -16
  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-manifest-ops.py +22 -6
  35. package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
  36. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  37. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  38. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  39. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  40. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  41. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  42. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  43. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  44. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  45. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  46. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  47. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  48. package/src/skf-analyze-source/SKILL.md +69 -16
  49. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  50. package/src/skf-analyze-source/customize.toml +55 -0
  51. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  52. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  53. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  54. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +48 -14
  55. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
  56. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  57. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  58. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +16 -10
  59. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  60. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  61. package/src/skf-audit-skill/SKILL.md +66 -15
  62. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  63. package/src/skf-audit-skill/customize.toml +49 -0
  64. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  65. package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +63 -19
  66. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  67. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  68. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  69. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  70. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  71. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +35 -17
  72. package/src/skf-brief-skill/SKILL.md +49 -19
  73. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  74. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  75. package/src/skf-brief-skill/customize.toml +44 -0
  76. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +27 -15
  77. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  78. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  79. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +35 -29
  80. package/src/skf-brief-skill/references/headless-args.md +9 -9
  81. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  82. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  83. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  84. package/src/skf-brief-skill/references/qmd-collection-registration.md +2 -2
  85. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
  86. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  87. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +29 -16
  88. package/src/skf-create-skill/SKILL.md +25 -19
  89. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  90. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  91. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  92. package/src/skf-create-skill/customize.toml +35 -0
  93. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  94. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  96. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  97. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  98. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  99. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  100. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
  101. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  102. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  103. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  104. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  105. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  106. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  107. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  108. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +48 -28
  109. package/src/skf-create-stack-skill/SKILL.md +76 -17
  110. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  111. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  112. package/src/skf-create-stack-skill/customize.toml +46 -0
  113. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  114. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  115. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +37 -11
  116. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -28
  117. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  118. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  119. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  120. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  122. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +44 -17
  123. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  124. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  125. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  126. package/src/skf-drop-skill/SKILL.md +84 -12
  127. package/src/skf-drop-skill/customize.toml +57 -0
  128. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  129. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  130. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  131. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  132. package/src/skf-export-skill/SKILL.md +85 -16
  133. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  134. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  135. package/src/skf-export-skill/customize.toml +49 -0
  136. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
  137. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  138. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
  139. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  140. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  141. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  142. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  143. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  144. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +79 -0
  145. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
  146. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  147. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
  148. package/src/skf-quick-skill/SKILL.md +63 -123
  149. package/src/skf-quick-skill/customize.toml +44 -0
  150. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  151. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  152. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  153. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  154. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  155. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  156. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  157. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  158. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  159. package/src/skf-refine-architecture/SKILL.md +86 -16
  160. package/src/skf-refine-architecture/customize.toml +49 -0
  161. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  162. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  163. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  164. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  165. package/src/skf-refine-architecture/references/init.md +144 -0
  166. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  167. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  168. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  169. package/src/skf-rename-skill/SKILL.md +82 -12
  170. package/src/skf-rename-skill/customize.toml +52 -0
  171. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  172. package/src/skf-rename-skill/references/health-check.md +30 -0
  173. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  174. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  175. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  176. package/src/skf-setup/SKILL.md +35 -35
  177. package/src/skf-setup/customize.toml +33 -0
  178. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  179. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
  180. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  181. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  182. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  183. package/src/skf-setup/references/tier-rules.md +1 -1
  184. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  185. package/src/skf-test-skill/SKILL.md +77 -15
  186. package/src/skf-test-skill/customize.toml +54 -0
  187. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +37 -73
  188. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  189. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  190. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  191. package/src/skf-test-skill/references/health-check.md +14 -0
  192. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  193. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  194. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  195. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  196. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  197. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  198. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  199. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  200. package/src/skf-update-skill/SKILL.md +23 -14
  201. package/src/skf-update-skill/customize.toml +44 -0
  202. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  203. package/src/skf-update-skill/references/health-check.md +32 -0
  204. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  205. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  206. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  207. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  208. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  209. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  210. package/src/skf-update-skill/references/report.md +214 -0
  211. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  212. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  213. package/src/skf-verify-stack/SKILL.md +89 -17
  214. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  215. package/src/skf-verify-stack/customize.toml +50 -0
  216. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  217. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +13 -9
  218. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  219. package/src/skf-verify-stack/references/init.md +170 -0
  220. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  221. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +18 -14
  222. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
  223. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
  224. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
  225. package/tools/validate-docs-drift.js +1 -1
  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
@@ -0,0 +1,576 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Disqualify Candidates — deterministic filter for skillable-unit boundaries.
6
+
7
+ `identify-units.md` step 2 §D asks the LLM to evaluate each detected boundary
8
+ against a fixed disqualification list (too-few-files, too-low-loc,
9
+ generated-code, auto-generated-tag, etc.). The boundary checks are fully
10
+ deterministic — file counts, line counts, path-substring matches against a
11
+ known generated-code roster, header sentinel scans — and prior runs have
12
+ shown LLMs miscount files, miss generated-code paths inside nested
13
+ `node_modules/`, and forget to scan for `@generated`. This script collapses
14
+ that work into one call.
15
+
16
+ Canonical disqualification rules (kept in sync with
17
+ `src/skf-analyze-source/references/unit-detection-heuristics.md`):
18
+
19
+ - too-few-files : files_count < {MIN_FILES} (default 3)
20
+ - too-low-loc : loc_total < {MIN_LOC} (default 100)
21
+ - generated-code : any file path contains a known generated/vendor
22
+ directory segment (node_modules/, vendor/, dist/,
23
+ build/, __pycache__/, .next/, target/, .gradle/)
24
+ - auto-generated-tag: any file's first 10 lines contain a known
25
+ "auto-generated" sentinel string (`@generated`,
26
+ `// GENERATED CODE`, `# AUTOGENERATED`,
27
+ `<!-- generated -->`); case-insensitive matching
28
+ on the full sentinel phrases.
29
+
30
+ Rules NOT enforced by this script (still LLM-judged in step 2 §D):
31
+ - pure-configuration
32
+ - test-only
33
+ - already-skilled (requires existing_skills cross-reference)
34
+
35
+ LoC counting: per file, count lines with NON-empty content. A "blank line"
36
+ is one whose stripped form is empty (whitespace-only lines count as blank).
37
+ This matches the spec phrasing "lines with content (strip empty lines)".
38
+
39
+ The check order matters because heuristics are reported as a single
40
+ `reason`; the first match wins. Order is:
41
+ 1. generated-code (path-based, cheapest, often the dominant signal)
42
+ 2. auto-generated-tag (header scan)
43
+ 3. too-few-files (file count after the above)
44
+ 4. too-low-loc (loc after the above)
45
+
46
+ Subcommand:
47
+ filter --boundaries <json-file-or--for-stdin> [--source-root <path>]
48
+ [--min-files N] [--min-loc N]
49
+
50
+ Read a boundaries JSON of the shape:
51
+ [
52
+ {"name": "<unit-name>",
53
+ "path": "<rel-from-source-root>",
54
+ "files": ["<rel-path>", ...]},
55
+ ...
56
+ ]
57
+ and emit:
58
+ {
59
+ "kept": [{"name": "...", "path": "...",
60
+ "files_count": N, "loc_total": L}, ...],
61
+ "dropped": [{"name": "...",
62
+ "reason": "<too-few-files|too-low-loc|generated-code|"
63
+ "auto-generated-tag>",
64
+ "context": {"files_count": N, "loc_total": L,
65
+ "first_match": "..."}}, ...],
66
+ "stats": {"kept": N, "dropped": N,
67
+ "by_reason": {"<reason>": N, ...}}
68
+ }
69
+
70
+ --source-root defaults to the current working directory. File paths in
71
+ the `files[]` array are resolved against `<source-root>/<boundary-path>`
72
+ first (to match `path` from the earlier scan-project step), and falling
73
+ back to `<source-root>/<file-rel>` if that doesn't exist. Missing files
74
+ are treated as 0-line zero-byte files; the boundary is still subject to
75
+ count/loc thresholds.
76
+
77
+ CLI examples:
78
+ uv run skf-disqualify-candidates.py filter --boundaries boundaries.json
79
+ cat boundaries.json | uv run skf-disqualify-candidates.py filter \\
80
+ --boundaries - --source-root /path/to/project
81
+
82
+ Exit codes:
83
+ 0 — operation succeeded (including: empty input → empty kept/dropped)
84
+ 1 — user error (bad JSON, missing required keys, malformed entry)
85
+ """
86
+
87
+ from __future__ import annotations
88
+
89
+ import argparse
90
+ import json
91
+ import sys
92
+ from pathlib import Path
93
+
94
+
95
+ # --------------------------------------------------------------------------
96
+ # Constants — disqualification rules (single source of truth)
97
+ # --------------------------------------------------------------------------
98
+
99
+
100
+ MIN_FILES = 3
101
+ MIN_LOC = 100
102
+
103
+ # Path segments that mark generated / vendored / build-output trees.
104
+ # Trailing slash is significant — we match the segment as a directory.
105
+ GENERATED_PATH_SEGMENTS = (
106
+ "node_modules/",
107
+ "vendor/",
108
+ "dist/",
109
+ "build/",
110
+ "__pycache__/",
111
+ ".next/",
112
+ "target/",
113
+ ".gradle/",
114
+ )
115
+
116
+ # Header-sentinel scan: case-insensitive substring match on each of the
117
+ # first AUTO_GEN_HEADER_LINES of a file. The strings are spec-canonical;
118
+ # we apply case-insensitive matching (compare to a lowercased line).
119
+ AUTO_GEN_SENTINELS = (
120
+ "@generated",
121
+ "// generated code",
122
+ "# autogenerated",
123
+ "<!-- generated -->",
124
+ )
125
+
126
+ AUTO_GEN_HEADER_LINES = 10
127
+
128
+
129
+ # Reason codes — exported as module-level constants so callers (and tests)
130
+ # can reference them without string typos.
131
+ REASON_TOO_FEW_FILES = "too-few-files"
132
+ REASON_TOO_LOW_LOC = "too-low-loc"
133
+ REASON_GENERATED_CODE = "generated-code"
134
+ REASON_AUTO_GENERATED_TAG = "auto-generated-tag"
135
+
136
+ ALL_REASONS = (
137
+ REASON_GENERATED_CODE,
138
+ REASON_AUTO_GENERATED_TAG,
139
+ REASON_TOO_FEW_FILES,
140
+ REASON_TOO_LOW_LOC,
141
+ )
142
+
143
+
144
+ # --------------------------------------------------------------------------
145
+ # Boundary validation
146
+ # --------------------------------------------------------------------------
147
+
148
+
149
+ def validate_boundaries(boundaries: object) -> list[dict]:
150
+ """Validate the boundaries input is a list of {name, path, files} records.
151
+
152
+ Returns the list with each entry's `files` normalized to forward-slash
153
+ strings. Raises ValueError on any structural defect.
154
+ """
155
+ if not isinstance(boundaries, list):
156
+ raise ValueError(
157
+ f"boundaries input must be a JSON array; got {type(boundaries).__name__}"
158
+ )
159
+ normalized: list[dict] = []
160
+ for idx, entry in enumerate(boundaries):
161
+ if not isinstance(entry, dict):
162
+ raise ValueError(
163
+ f"boundary entry at index {idx} is not an object: {entry!r}"
164
+ )
165
+ name = entry.get("name")
166
+ if not isinstance(name, str) or not name:
167
+ raise ValueError(
168
+ f"boundary entry at index {idx} missing required `name` field"
169
+ )
170
+ path = entry.get("path")
171
+ if not isinstance(path, str):
172
+ raise ValueError(
173
+ f"boundary entry {name!r} missing required `path` field"
174
+ )
175
+ files = entry.get("files")
176
+ if not isinstance(files, list):
177
+ raise ValueError(
178
+ f"boundary entry {name!r} missing required `files` array"
179
+ )
180
+ norm_files: list[str] = []
181
+ for f_idx, f in enumerate(files):
182
+ if not isinstance(f, str):
183
+ raise ValueError(
184
+ f"boundary {name!r} files[{f_idx}] is not a string: {f!r}"
185
+ )
186
+ norm_files.append(f.replace("\\", "/"))
187
+ normalized.append({
188
+ "name": name,
189
+ "path": path.replace("\\", "/"),
190
+ "files": norm_files,
191
+ })
192
+ return normalized
193
+
194
+
195
+ # --------------------------------------------------------------------------
196
+ # Path-based detection (generated-code)
197
+ # --------------------------------------------------------------------------
198
+
199
+
200
+ def find_generated_segment(file_path: str) -> str | None:
201
+ """Return the first generated-path segment that appears in `file_path`,
202
+ or None if none match.
203
+
204
+ The match is on the raw string with forward slashes. We compare against
205
+ `<segment>` at any position (start or interior); a `node_modules/` at
206
+ the root counts the same as one nested deeper.
207
+ """
208
+ norm = file_path.replace("\\", "/")
209
+ # accept matches at the very start (no leading slash needed) too —
210
+ # so `node_modules/foo.js` and `pkg/node_modules/foo.js` both hit.
211
+ bracketed = "/" + norm # so segments can match at the head consistently
212
+ for seg in GENERATED_PATH_SEGMENTS:
213
+ if "/" + seg in bracketed:
214
+ return seg
215
+ return None
216
+
217
+
218
+ # --------------------------------------------------------------------------
219
+ # Header-sentinel scan (auto-generated-tag)
220
+ # --------------------------------------------------------------------------
221
+
222
+
223
+ _AUTO_GEN_SENTINELS_LC = tuple(s.lower() for s in AUTO_GEN_SENTINELS)
224
+
225
+
226
+ def has_auto_generated_marker(file_abs: Path) -> str | None:
227
+ """Scan the first AUTO_GEN_HEADER_LINES lines of `file_abs` for any of
228
+ the AUTO_GEN_SENTINELS (case-insensitive substring match).
229
+
230
+ Returns the matched sentinel (lowercased) on first hit, or None.
231
+ Unreadable / non-UTF-8 files return None — header-sentinel detection
232
+ is opportunistic; binary blobs will simply not match.
233
+ """
234
+ try:
235
+ with file_abs.open("r", encoding="utf-8", errors="replace") as fh:
236
+ for i, raw_line in enumerate(fh):
237
+ if i >= AUTO_GEN_HEADER_LINES:
238
+ break
239
+ lc = raw_line.lower()
240
+ for sentinel in _AUTO_GEN_SENTINELS_LC:
241
+ if sentinel in lc:
242
+ return sentinel
243
+ except OSError:
244
+ return None
245
+ return None
246
+
247
+
248
+ # --------------------------------------------------------------------------
249
+ # LoC counting
250
+ # --------------------------------------------------------------------------
251
+
252
+
253
+ def count_nonblank_lines(file_abs: Path) -> int:
254
+ """Count lines whose stripped form is non-empty. Whitespace-only lines
255
+ count as blank. Returns 0 for unreadable files.
256
+ """
257
+ try:
258
+ with file_abs.open("r", encoding="utf-8", errors="replace") as fh:
259
+ n = 0
260
+ for line in fh:
261
+ if line.strip():
262
+ n += 1
263
+ return n
264
+ except OSError:
265
+ return 0
266
+
267
+
268
+ # --------------------------------------------------------------------------
269
+ # Per-boundary evaluation
270
+ # --------------------------------------------------------------------------
271
+
272
+
273
+ def _resolve_file(source_root: Path, boundary_path: str, file_rel: str) -> Path:
274
+ """Resolve a boundary's `files[]` entry to an absolute path.
275
+
276
+ Tries in order:
277
+ 1. <source_root>/<file_rel> (file_rel already source-root-relative)
278
+ 2. <source_root>/<boundary_path>/<file_rel> (file_rel boundary-relative)
279
+
280
+ The first existing path wins; otherwise returns the source-root option
281
+ so downstream code can decide what "missing" means.
282
+ """
283
+ candidate_a = source_root / file_rel
284
+ if candidate_a.is_file():
285
+ return candidate_a
286
+ candidate_b = source_root / boundary_path / file_rel
287
+ if candidate_b.is_file():
288
+ return candidate_b
289
+ # neither exists — return the source-root form for missing-file accounting
290
+ return candidate_a
291
+
292
+
293
+ def evaluate_boundary(
294
+ boundary: dict,
295
+ *,
296
+ source_root: Path,
297
+ min_files: int = MIN_FILES,
298
+ min_loc: int = MIN_LOC,
299
+ ) -> dict:
300
+ """Evaluate a single boundary against all disqualification rules.
301
+
302
+ Returns a record:
303
+ - kept: {"name", "path", "files_count", "loc_total"}
304
+ - dropped: {"name", "reason", "context": {...}}
305
+
306
+ Check order:
307
+ 1. generated-code (path segment match)
308
+ 2. auto-generated-tag (header sentinel scan)
309
+ 3. too-few-files (count < min_files)
310
+ 4. too-low-loc (sum < min_loc)
311
+ """
312
+ name = boundary["name"]
313
+ path = boundary["path"]
314
+ files = boundary["files"]
315
+ files_count = len(files)
316
+
317
+ # 1. generated-code — path-substring match (cheapest signal)
318
+ for f in files:
319
+ seg = find_generated_segment(f)
320
+ if seg:
321
+ return {
322
+ "kind": "dropped",
323
+ "record": {
324
+ "name": name,
325
+ "reason": REASON_GENERATED_CODE,
326
+ "context": {
327
+ "files_count": files_count,
328
+ "first_match": f,
329
+ "matched_segment": seg,
330
+ },
331
+ },
332
+ }
333
+
334
+ # 2. auto-generated-tag — header sentinel scan
335
+ for f in files:
336
+ abs_path = _resolve_file(source_root, path, f)
337
+ sentinel = has_auto_generated_marker(abs_path)
338
+ if sentinel:
339
+ return {
340
+ "kind": "dropped",
341
+ "record": {
342
+ "name": name,
343
+ "reason": REASON_AUTO_GENERATED_TAG,
344
+ "context": {
345
+ "files_count": files_count,
346
+ "first_match": f,
347
+ "matched_sentinel": sentinel,
348
+ },
349
+ },
350
+ }
351
+
352
+ # Compute loc_total for the remaining checks (and for kept records).
353
+ loc_total = 0
354
+ for f in files:
355
+ abs_path = _resolve_file(source_root, path, f)
356
+ loc_total += count_nonblank_lines(abs_path)
357
+
358
+ # 3. too-few-files
359
+ if files_count < min_files:
360
+ return {
361
+ "kind": "dropped",
362
+ "record": {
363
+ "name": name,
364
+ "reason": REASON_TOO_FEW_FILES,
365
+ "context": {
366
+ "files_count": files_count,
367
+ "loc_total": loc_total,
368
+ "threshold": min_files,
369
+ },
370
+ },
371
+ }
372
+
373
+ # 4. too-low-loc
374
+ if loc_total < min_loc:
375
+ return {
376
+ "kind": "dropped",
377
+ "record": {
378
+ "name": name,
379
+ "reason": REASON_TOO_LOW_LOC,
380
+ "context": {
381
+ "files_count": files_count,
382
+ "loc_total": loc_total,
383
+ "threshold": min_loc,
384
+ },
385
+ },
386
+ }
387
+
388
+ return {
389
+ "kind": "kept",
390
+ "record": {
391
+ "name": name,
392
+ "path": path,
393
+ "files_count": files_count,
394
+ "loc_total": loc_total,
395
+ },
396
+ }
397
+
398
+
399
+ # --------------------------------------------------------------------------
400
+ # Top-level filter
401
+ # --------------------------------------------------------------------------
402
+
403
+
404
+ def filter_boundaries(
405
+ boundaries: list[dict],
406
+ *,
407
+ source_root: Path,
408
+ min_files: int = MIN_FILES,
409
+ min_loc: int = MIN_LOC,
410
+ ) -> dict:
411
+ """Run evaluate_boundary on each entry; aggregate kept/dropped/stats."""
412
+ kept: list[dict] = []
413
+ dropped: list[dict] = []
414
+ by_reason: dict[str, int] = {r: 0 for r in ALL_REASONS}
415
+
416
+ for b in boundaries:
417
+ result = evaluate_boundary(
418
+ b,
419
+ source_root=source_root,
420
+ min_files=min_files,
421
+ min_loc=min_loc,
422
+ )
423
+ if result["kind"] == "kept":
424
+ kept.append(result["record"])
425
+ else:
426
+ dropped.append(result["record"])
427
+ by_reason[result["record"]["reason"]] += 1
428
+
429
+ # prune zero-count reasons from by_reason — keeps the report tight
430
+ by_reason_compact = {r: n for r, n in by_reason.items() if n > 0}
431
+
432
+ return {
433
+ "kept": kept,
434
+ "dropped": dropped,
435
+ "stats": {
436
+ "kept": len(kept),
437
+ "dropped": len(dropped),
438
+ "by_reason": by_reason_compact,
439
+ },
440
+ }
441
+
442
+
443
+ # --------------------------------------------------------------------------
444
+ # CLI
445
+ # --------------------------------------------------------------------------
446
+
447
+
448
+ def _read_boundaries_source(source: str) -> object:
449
+ """Read boundaries JSON from a file path or stdin (if source == '-')."""
450
+ if source == "-":
451
+ try:
452
+ text = sys.stdin.read()
453
+ except OSError as exc:
454
+ raise ValueError(
455
+ f"failed to read boundaries JSON from stdin: {exc}"
456
+ ) from exc
457
+ else:
458
+ path = Path(source)
459
+ if not path.is_file():
460
+ raise ValueError(f"boundaries file not found: {path}")
461
+ try:
462
+ text = path.read_text(encoding="utf-8")
463
+ except OSError as exc:
464
+ raise ValueError(
465
+ f"failed to read boundaries file {path}: {exc}"
466
+ ) from exc
467
+ try:
468
+ return json.loads(text)
469
+ except json.JSONDecodeError as exc:
470
+ raise ValueError(f"malformed JSON in boundaries input: {exc}") from exc
471
+
472
+
473
+ def _cmd_filter(args: argparse.Namespace) -> int:
474
+ try:
475
+ raw = _read_boundaries_source(args.boundaries)
476
+ boundaries = validate_boundaries(raw)
477
+ except ValueError as exc:
478
+ print(f"error: {exc}", file=sys.stderr)
479
+ return 1
480
+
481
+ source_root = Path(args.source_root or ".").resolve()
482
+ if not source_root.is_dir():
483
+ print(
484
+ f"error: source-root not a directory: {source_root}", file=sys.stderr
485
+ )
486
+ return 1
487
+
488
+ if args.min_files < 0:
489
+ print(
490
+ f"error: --min-files must be >= 0; got {args.min_files}",
491
+ file=sys.stderr,
492
+ )
493
+ return 1
494
+ if args.min_loc < 0:
495
+ print(
496
+ f"error: --min-loc must be >= 0; got {args.min_loc}",
497
+ file=sys.stderr,
498
+ )
499
+ return 1
500
+
501
+ result = filter_boundaries(
502
+ boundaries,
503
+ source_root=source_root,
504
+ min_files=args.min_files,
505
+ min_loc=args.min_loc,
506
+ )
507
+ json.dump(result, sys.stdout, indent=2)
508
+ sys.stdout.write("\n")
509
+ return 0
510
+
511
+
512
+ def _build_parser() -> argparse.ArgumentParser:
513
+ parser = argparse.ArgumentParser(
514
+ prog="skf-disqualify-candidates",
515
+ description=(
516
+ "Filter detected boundaries against the unit-detection "
517
+ "disqualification list (too-few-files, too-low-loc, "
518
+ "generated-code, auto-generated-tag)."
519
+ ),
520
+ )
521
+ sub = parser.add_subparsers(dest="cmd", required=True)
522
+
523
+ p_filter = sub.add_parser(
524
+ "filter",
525
+ help="emit kept/dropped/stats partition as JSON",
526
+ )
527
+ p_filter.add_argument(
528
+ "--boundaries",
529
+ required=True,
530
+ help=(
531
+ "path to boundaries JSON, or '-' to read from stdin. "
532
+ "Shape: [{\"name\": \"<unit>\", \"path\": \"<rel>\", "
533
+ "\"files\": [\"<rel>\", ...]}, ...]"
534
+ ),
535
+ )
536
+ p_filter.add_argument(
537
+ "--source-root",
538
+ default=None,
539
+ help=(
540
+ "project root for resolving file paths in `files[]` "
541
+ "(default: current working directory). Each file is resolved as "
542
+ "<source-root>/<file-rel> first, falling back to "
543
+ "<source-root>/<boundary-path>/<file-rel>."
544
+ ),
545
+ )
546
+ p_filter.add_argument(
547
+ "--min-files",
548
+ type=int,
549
+ default=MIN_FILES,
550
+ help=(
551
+ f"minimum files for too-few-files (default {MIN_FILES}; "
552
+ "matches unit-detection-heuristics.md)"
553
+ ),
554
+ )
555
+ p_filter.add_argument(
556
+ "--min-loc",
557
+ type=int,
558
+ default=MIN_LOC,
559
+ help=(
560
+ f"minimum non-blank LoC for too-low-loc (default {MIN_LOC}; "
561
+ "matches unit-detection-heuristics.md)"
562
+ ),
563
+ )
564
+ p_filter.set_defaults(func=_cmd_filter)
565
+
566
+ return parser
567
+
568
+
569
+ def main(argv: list[str] | None = None) -> int:
570
+ parser = _build_parser()
571
+ args = parser.parse_args(argv)
572
+ return args.func(args)
573
+
574
+
575
+ if __name__ == "__main__":
576
+ raise SystemExit(main())
@@ -4,8 +4,8 @@
4
4
  # ///
5
5
  """SKF Emit Brief Result Envelope — Schema-locked headless output for skf-brief-skill.
6
6
 
7
- Replaces the prose-driven envelope assembly in `src/skf-brief-skill/steps-c/
8
- step-05-write-brief.md` §4b with one Python invocation. The envelope contract
7
+ Replaces the prose-driven envelope assembly in `src/skf-brief-skill/references/
8
+ write-brief.md` §4b with one Python invocation. The envelope contract
9
9
  (SKF_BRIEF_RESULT_JSON) is documented in src/skf-brief-skill/SKILL.md Result
10
10
  Contract section.
11
11
 
@@ -239,7 +239,7 @@ def main() -> int:
239
239
  "--target",
240
240
  choices=["stdout", "stderr"],
241
241
  default="stdout",
242
- help="Output stream for the prefixed envelope line. step-05 §4b uses stdout on success and stderr on HARD HALT.",
242
+ help="Output stream for the prefixed envelope line. step 5 §4b uses stdout on success and stderr on HARD HALT.",
243
243
  )
244
244
 
245
245
  sub.add_parser("validate", help="Read envelope JSON on stdin, exit 0 if schema-valid")