bmad-module-skill-forge 2.2.0 → 4.0.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 (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +143 -18
  18. package/docs/verifying-a-skill.md +87 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +3 -3
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +143 -20
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +155 -14
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +23 -2
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +28 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -1,18 +1,25 @@
1
1
  # /// script
2
- # requires-python = ">=3.9"
3
- # dependencies = []
2
+ # requires-python = ">=3.11"
3
+ # dependencies = ["pyyaml"]
4
4
  # ///
5
5
  """SKF Merge Doc URLs — merge corpora-seeded + detected docs for a skill brief.
6
6
 
7
7
  Deterministic merge of an upstream brief's `doc_urls` (registry-seeded
8
8
  companion corpora, source: language-registry) with the docs freshly
9
9
  discovered by `skf-detect-docs.py` (mapped to the brief `{url, label, source}`
10
- contract). Replaces the prose dedup/suppression that lived inline in
11
- `skf-brief-skill/references/step-auto-brief.md` §3 — that logic is fiddly
10
+ contract). Replaces the prose mapping, dedup and suppression that lived inline
11
+ in `skf-brief-skill/references/step-auto-brief.md` §3. That logic is fiddly
12
12
  (normalized-URL collisions, non-corpus path segments, locale duplicates) and
13
13
  must behave identically on every run, so it belongs in a tested script.
14
14
 
15
- Three operations, in order:
15
+ With --detected-file, each skf-detect-docs.py entry {url, detected_via,
16
+ content_type} is first mapped to {url, label, source}: `source` from
17
+ `detected_via` and `label` from `content_type`, by the two tables below (a
18
+ content_type they do not list gives the label ""). An entry whose
19
+ `detected_via` they do not list is never merged: `suppressed` lists it with
20
+ reason unknown-detected-via.
21
+
22
+ Three merge operations, in order:
16
23
 
17
24
  1. Normalized-URL dedup — lowercase host, strip a trailing `/index.html`
18
25
  and any trailing `/` before comparing, so a seeded `…/book/` and a
@@ -41,10 +48,18 @@ DEGRADED case is intentionally out of scope (there is no canonical corpus host
41
48
  to anchor suppression against).
42
49
 
43
50
  CLI:
51
+ uv run src/shared/scripts/skf-merge-doc-urls.py --scope-type <type> \
52
+ --detected-file <detect-docs.json> --existing-brief <skill-brief.yaml>
44
53
  echo '{"scope_type": "...", "existing": [...], "detected": [...]}' \
45
54
  | uv run src/shared/scripts/skf-merge-doc-urls.py
46
55
 
47
- Input (JSON object on stdin):
56
+ Files form (stdin is not read): --detected-file holds skf-detect-docs.py's
57
+ JSON array, mapped as above; --existing-brief is a skill-brief.yaml whose
58
+ `doc_urls` are the existing entries (none when it has no doc_urls), read as
59
+ skf-resolve-authoritative-files.py reads a brief; --scope-type is the scope
60
+ type. Each flag is optional: an absent one gives no entries (or "").
61
+
62
+ Input of the stdin form (a JSON object), whose entries are merged as given:
48
63
  {
49
64
  "scope_type": "full-library",
50
65
  "existing": [{"url": "...", "label": "...", "source": "language-registry"}, ...],
@@ -54,22 +69,42 @@ Input (JSON object on stdin):
54
69
  Output (JSON object on stdout):
55
70
  {
56
71
  "doc_urls": [{"url": "...", "label": "...", "source": "..."}, ...],
57
- "suppressed": [{"url": "...", "reason": "non-corpus-segment|non-primary-locale-dup"}, ...]
72
+ "suppressed": [{"url": "...", "reason":
73
+ "unknown-detected-via|non-corpus-segment|non-primary-locale-dup"}, ...]
58
74
  }
59
75
 
60
76
  Exit codes:
61
77
  0 success (merged list emitted)
62
- 2 error (bad JSON, wrong input shape)
78
+ 2 error (bad JSON, wrong input shape, a file that cannot be read, a brief
79
+ that is not a YAML mapping)
63
80
  """
64
81
 
65
82
  from __future__ import annotations
66
83
 
84
+ import argparse
85
+ import importlib.util
67
86
  import json
68
87
  import re
69
88
  import sys
89
+ from pathlib import Path
70
90
  from typing import Any
71
91
  from urllib.parse import urlsplit
72
92
 
93
+ # skf-detect-docs.py's `detected_via` -> the brief's coarse provenance, the
94
+ # skill-brief.v1.json doc_urls[].source enum (a test pins it to the schema).
95
+ _SOURCE_BY_DETECTED_VIA = {
96
+ "homepageUrl": "homepage",
97
+ "readme_link": "readme-detection",
98
+ "pages_api": "pages-api",
99
+ "docs_folder": "docs-folder",
100
+ }
101
+ # skf-detect-docs.py's `content_type` -> the entry's display label.
102
+ _LABEL_BY_CONTENT_TYPE = {
103
+ "api-docs": "API Documentation",
104
+ "guide": "Guide",
105
+ "reference": "Reference",
106
+ }
107
+
73
108
  # Path components (case-insensitive) that mark a non-corpus page on a doc host:
74
109
  # changelogs/news, contribution guides, wikis. Matched as whole path components
75
110
  # (or component + a separator), never as a bare substring.
@@ -153,6 +188,28 @@ def _matches_non_corpus_segment(path: str) -> bool:
153
188
  return False
154
189
 
155
190
 
191
+ def map_detected(raw: list[Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
192
+ """skf-detect-docs.py entries as brief doc_urls entries {url, label, source}.
193
+
194
+ Returns (mapped, suppressed): an entry whose detected_via maps to no
195
+ source is suppressed with reason unknown-detected-via; one with no url is
196
+ skipped, as the merge skips it.
197
+ """
198
+ mapped: list[dict[str, Any]] = []
199
+ suppressed: list[dict[str, Any]] = []
200
+ for d in raw:
201
+ if not isinstance(d, dict) or not d.get("url"):
202
+ continue
203
+ via, content_type = d.get("detected_via"), d.get("content_type")
204
+ source = _SOURCE_BY_DETECTED_VIA.get(via) if isinstance(via, str) else None
205
+ if source is None:
206
+ suppressed.append({"url": d["url"], "reason": "unknown-detected-via"})
207
+ continue
208
+ label = _LABEL_BY_CONTENT_TYPE.get(content_type, "") if isinstance(content_type, str) else ""
209
+ mapped.append({"url": d["url"], "label": label, "source": source})
210
+ return mapped, suppressed
211
+
212
+
156
213
  def merge_doc_urls(
157
214
  scope_type: str,
158
215
  existing: list[dict[str, Any]],
@@ -232,30 +289,96 @@ def _force_utf8(*streams) -> None:
232
289
  stream.reconfigure(encoding="utf-8", errors=errors)
233
290
 
234
291
 
235
- def main() -> int:
236
- _force_utf8(sys.stdin, sys.stdout)
292
+ class InputError(Exception):
293
+ """An input the merge cannot read (exit 2)."""
294
+
295
+
296
+ def _read_detected(path: str) -> list[Any]:
297
+ """The JSON array skf-detect-docs.py wrote to `path`."""
298
+ try:
299
+ data = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
300
+ except OSError as exc:
301
+ raise InputError(f"cannot read --detected-file {path}: {exc.strerror or exc}") from exc
302
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
303
+ raise InputError(f"--detected-file {path} is not JSON: {exc}") from exc
304
+ if not isinstance(data, list):
305
+ raise InputError(f"--detected-file {path} must hold a JSON array (skf-detect-docs.py output)")
306
+ return data
307
+
308
+
309
+ def _brief_doc_urls(path: str) -> list[Any]:
310
+ """The doc_urls of the skill brief at `path`, read by
311
+ skf-resolve-authoritative-files.py's parse_brief_yaml (beside this file)."""
312
+ resolver_path = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
313
+ spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", resolver_path)
314
+ if spec is None or spec.loader is None:
315
+ raise InputError(f"cannot load {resolver_path.name} beside {Path(__file__).name}")
316
+ resolver = importlib.util.module_from_spec(spec)
317
+ try:
318
+ spec.loader.exec_module(resolver)
319
+ except (OSError, ImportError, SyntaxError) as exc:
320
+ raise InputError(f"cannot load {resolver_path.name} beside {Path(__file__).name}: {exc}") from exc
321
+ try:
322
+ brief = resolver.parse_brief_yaml(Path(path).read_bytes().decode("utf-8-sig"))
323
+ except OSError as exc:
324
+ raise InputError(f"cannot read --existing-brief {path}: {exc.strerror or exc}") from exc
325
+ except (UnicodeDecodeError, resolver.yaml.YAMLError) as exc:
326
+ raise InputError(f"--existing-brief {path} is not YAML: {exc}") from exc
327
+ if not isinstance(brief, dict):
328
+ raise InputError(f"--existing-brief {path} holds no brief mapping")
329
+ doc_urls = brief.get("doc_urls") or []
330
+ if not isinstance(doc_urls, list):
331
+ raise InputError(f"the doc_urls of --existing-brief {path} must be a list")
332
+ return doc_urls
333
+
334
+
335
+ def _stdin_payload() -> tuple[str, list[Any], list[Any]]:
336
+ """(scope_type, existing, detected) of the stdin form."""
237
337
  raw = sys.stdin.read()
238
338
  if not raw or not raw.strip():
239
- sys.stderr.write("skf-merge-doc-urls: empty stdin (expected JSON object)\n")
240
- return 2
339
+ raise InputError("empty stdin (expected JSON object)")
241
340
  try:
242
341
  payload = json.loads(raw)
243
342
  except json.JSONDecodeError as exc:
244
- sys.stderr.write(f"skf-merge-doc-urls: invalid JSON on stdin: {exc}\n")
245
- return 2
343
+ raise InputError(f"invalid JSON on stdin: {exc}") from exc
246
344
  if not isinstance(payload, dict):
247
- sys.stderr.write("skf-merge-doc-urls: input must be a JSON object\n")
248
- return 2
249
-
250
- scope_type = payload.get("scope_type") or ""
345
+ raise InputError("input must be a JSON object")
251
346
  existing = payload.get("existing") or []
252
347
  detected = payload.get("detected") or []
253
348
  if not isinstance(existing, list) or not isinstance(detected, list):
254
- sys.stderr.write("skf-merge-doc-urls: 'existing' and 'detected' must be arrays\n")
349
+ raise InputError("'existing' and 'detected' must be arrays")
350
+ return payload.get("scope_type") or "", existing, detected
351
+
352
+
353
+ def main(argv: list[str] | None = None) -> int:
354
+ _force_utf8(sys.stdin, sys.stdout)
355
+ parser = argparse.ArgumentParser(
356
+ prog="skf-merge-doc-urls",
357
+ description="Merge a brief's doc_urls with the docs skf-detect-docs.py found. With no flag, read the "
358
+ "{scope_type, existing, detected} JSON object on stdin.",
359
+ )
360
+ parser.add_argument("--detected-file", metavar="PATH",
361
+ help="skf-detect-docs.py's JSON array, mapped to {url, label, source}")
362
+ parser.add_argument("--existing-brief", metavar="PATH",
363
+ help="the skill-brief.yaml whose doc_urls are the existing entries")
364
+ parser.add_argument("--scope-type", metavar="TYPE", help="the brief's scope.type")
365
+ args = parser.parse_args(argv)
366
+ unknown: list[dict[str, Any]] = []
367
+ try:
368
+ if args.detected_file is None and args.existing_brief is None and args.scope_type is None:
369
+ scope_type, existing, detected = _stdin_payload()
370
+ else:
371
+ scope_type = args.scope_type or ""
372
+ existing = _brief_doc_urls(args.existing_brief) if args.existing_brief is not None else []
373
+ detected = []
374
+ if args.detected_file is not None:
375
+ detected, unknown = map_detected(_read_detected(args.detected_file))
376
+ except InputError as exc:
377
+ sys.stderr.write(f"skf-merge-doc-urls: {exc}\n")
255
378
  return 2
256
379
 
257
380
  doc_urls, suppressed = merge_doc_urls(scope_type, existing, detected)
258
- json.dump({"doc_urls": doc_urls, "suppressed": suppressed}, sys.stdout, ensure_ascii=False)
381
+ json.dump({"doc_urls": doc_urls, "suppressed": unknown + suppressed}, sys.stdout, ensure_ascii=False)
259
382
  sys.stdout.write("\n")
260
383
  return 0
261
384
 
@@ -0,0 +1,221 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Names Present: the provenance entries whose export name a skill package never writes.
6
+
7
+ skf-test-skill credits a provenance entry toward a stack's Export Coverage
8
+ only when the package writes its `export_name`: reconcile-coverage.py
9
+ (`load_doc_text`, `names_present`) joins the text of `SKILL.md` and of each
10
+ `.md` file directly in `references/` (sorted, read as UTF-8, no subfolder)
11
+ and looks for the name in it as a fixed string, case-sensitive, never inside
12
+ a longer identifier: on a side where the name ends in a letter, digit, `_`
13
+ or `$`, the next character may not be one of those, so `get` is not found in
14
+ `target` or `getAll` (skf-test-skill's validate-inventory.py keeps that
15
+ rule; NAME_CHARS here must stay the same). A name holding
16
+ `::` (an impl-block method, `Type::method`) rolls up under its type and is
17
+ never looked up, and an entry with no name is no cited contract.
18
+ create-stack-skill (generate-output section 8) runs this helper on a
19
+ compose-mode map before the commit, so a stack never commits a name
20
+ skf-test-skill will not credit. The rule here is the scorer's:
21
+ test/test-skf-stack-step-rules.py runs both on the same package.
22
+
23
+ Usage:
24
+ uv run skf-names-present.py --provenance <provenance-map.json> \\
25
+ --skill-dir <dir> [--drop-absent]
26
+
27
+ Output (stdout):
28
+ {
29
+ "checked": <int>, # entries whose export_name was looked up
30
+ "skipped": <int>, # entries with a `::` name or no name
31
+ "absent": [ # in entries[] order
32
+ {"entry_index": <int>, "export_name": "<name>",
33
+ "source_library": "<library>" | null},
34
+ ...
35
+ ],
36
+ "dropped": [...], # --drop-absent: the absent entries removed
37
+ "provenance_written": <bool>
38
+ }
39
+
40
+ --drop-absent removes every absent entry from the map and rewrites it
41
+ through skf-atomic-write.py beside this script (JSON indented by two
42
+ spaces, non-ASCII kept, a final newline). `absent[]` still lists what the
43
+ lookup found, and `dropped[]` repeats it. `entry_index` is the position in
44
+ the map as it was read.
45
+
46
+ Exit codes:
47
+ 0 no entry is absent, or --drop-absent removed every absent entry
48
+ 1 an entry is absent (absent[] lists it)
49
+ 2 usage, file, JSON or write error (one line on stderr, no JSON)
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ import argparse
55
+ import json
56
+ import re
57
+ import subprocess
58
+ import sys
59
+ from pathlib import Path
60
+
61
+ ATOMIC_WRITE_HELPER = "skf-atomic-write.py"
62
+ ATOMIC_WRITE_TIMEOUT_SEC = 60
63
+ METHOD_SEPARATOR = "::"
64
+ # Characters that continue an identifier in the languages SKF documents (`$`
65
+ # for JS/TS names such as `$state`): skf-test-skill's IDENT_CHARS.
66
+ NAME_CHARS = r"\w$"
67
+ _NAME_CHAR_RE = re.compile(f"[{NAME_CHARS}]")
68
+
69
+
70
+ def load_doc_text(skill_dir: Path) -> str:
71
+ """The text skf-test-skill looks names up in: SKILL.md, then each .md file
72
+ directly in references/ in sorted order, joined by newlines. Raises
73
+ OSError, or UnicodeDecodeError for a file that is not UTF-8."""
74
+ parts = []
75
+ skill_md = skill_dir / "SKILL.md"
76
+ if skill_md.is_file():
77
+ parts.append(skill_md.read_text(encoding="utf-8"))
78
+ refs = skill_dir / "references"
79
+ if refs.is_dir():
80
+ for ref in sorted(refs.glob("*.md")):
81
+ parts.append(ref.read_text(encoding="utf-8"))
82
+ return "\n".join(parts)
83
+
84
+
85
+ def name_written(name: str, doc_text: str) -> bool:
86
+ """True when `doc_text` holds `name` as skf-test-skill's scorer finds it."""
87
+ body = re.escape(name)
88
+ if _NAME_CHAR_RE.match(name[0]):
89
+ body = f"(?<![{NAME_CHARS}])" + body
90
+ if _NAME_CHAR_RE.match(name[-1]):
91
+ body += f"(?![{NAME_CHARS}])"
92
+ return re.search(body, doc_text) is not None
93
+
94
+
95
+ def looked_up_name(entry: object) -> str | None:
96
+ """The name skf-test-skill looks up for an entry, or None when it looks
97
+ up none: no string name, or a `Type::method` name."""
98
+ if not isinstance(entry, dict):
99
+ return None
100
+ name = entry.get("export_name")
101
+ if not isinstance(name, str) or not name or METHOD_SEPARATOR in name:
102
+ return None
103
+ return name
104
+
105
+
106
+ def find_absent(entries: list, doc_text: str) -> tuple[int, list[dict]]:
107
+ """(entries looked up, the absent ones in entries[] order)."""
108
+ checked, absent = 0, []
109
+ for index, entry in enumerate(entries):
110
+ name = looked_up_name(entry)
111
+ if name is None:
112
+ continue
113
+ checked += 1
114
+ if not name_written(name, doc_text):
115
+ library = entry.get("source_library")
116
+ absent.append({
117
+ "entry_index": index,
118
+ "export_name": name,
119
+ "source_library": library if isinstance(library, str) else None,
120
+ })
121
+ return checked, absent
122
+
123
+
124
+ def atomic_write(target: Path, data: bytes) -> None:
125
+ """Write `data` to `target` through skf-atomic-write.py beside this
126
+ script. Raises OSError when the helper is missing or fails."""
127
+ helper = Path(__file__).resolve().parent / ATOMIC_WRITE_HELPER
128
+ if not helper.is_file():
129
+ raise OSError(f"{ATOMIC_WRITE_HELPER} not found beside {Path(__file__).name}; nothing written")
130
+ try:
131
+ result = subprocess.run(
132
+ [sys.executable, str(helper), "write", "--target", str(target)],
133
+ input=data,
134
+ capture_output=True,
135
+ timeout=ATOMIC_WRITE_TIMEOUT_SEC,
136
+ check=False,
137
+ )
138
+ except (OSError, subprocess.SubprocessError) as exc:
139
+ raise OSError(f"atomic write of {target} failed: {exc}") from exc
140
+ if result.returncode != 0:
141
+ detail = (result.stderr or b"").decode("utf-8", errors="replace").strip()
142
+ raise OSError(f"atomic write of {target} failed: {detail}")
143
+
144
+
145
+ def _fail(message: str) -> int:
146
+ print(f"error: {' '.join(message.split())}", file=sys.stderr)
147
+ return 2
148
+
149
+
150
+ def _build_parser() -> argparse.ArgumentParser:
151
+ parser = argparse.ArgumentParser(
152
+ prog="skf-names-present",
153
+ description=(
154
+ "List the provenance entries whose export_name the skill package "
155
+ "never writes, by skf-test-skill's Export Coverage rule "
156
+ "(SKILL.md and references/*.md, case-sensitive substring, `::` "
157
+ "names skipped); with --drop-absent, remove them from the map."
158
+ ),
159
+ )
160
+ parser.add_argument("--provenance", required=True, help="path to provenance-map.json")
161
+ parser.add_argument(
162
+ "--skill-dir",
163
+ required=True,
164
+ help="skill package folder holding the SKILL.md and references/ to search",
165
+ )
166
+ parser.add_argument(
167
+ "--drop-absent",
168
+ action="store_true",
169
+ help="remove every absent entry and rewrite the map through skf-atomic-write.py",
170
+ )
171
+ return parser
172
+
173
+
174
+ def main(argv: list[str] | None = None) -> int:
175
+ args = _build_parser().parse_args(argv)
176
+ prov_path = Path(args.provenance)
177
+ skill_dir = Path(args.skill_dir)
178
+ if not prov_path.is_file():
179
+ return _fail(f"provenance map not found: {prov_path}")
180
+ # A wrong folder would read every name as absent, and --drop-absent
181
+ # would then empty the map: require the package's SKILL.md.
182
+ if not (skill_dir / "SKILL.md").is_file():
183
+ return _fail(f"skill dir has no SKILL.md: {skill_dir}")
184
+ try:
185
+ prov = json.loads(prov_path.read_text(encoding="utf-8"))
186
+ except (OSError, ValueError) as exc:
187
+ return _fail(f"cannot read the provenance map {prov_path}: {exc}")
188
+ entries = prov.get("entries") if isinstance(prov, dict) else None
189
+ if not isinstance(entries, list):
190
+ return _fail(f"the provenance map {prov_path} has no entries[] list")
191
+ try:
192
+ doc_text = load_doc_text(skill_dir)
193
+ except (OSError, ValueError) as exc:
194
+ return _fail(f"cannot read the skill package {skill_dir}: {exc}")
195
+
196
+ checked, absent = find_absent(entries, doc_text)
197
+ dropped: list[dict] = []
198
+ if args.drop_absent and absent:
199
+ gone = {item["entry_index"] for item in absent}
200
+ prov["entries"] = [entry for index, entry in enumerate(entries) if index not in gone]
201
+ data = (json.dumps(prov, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
202
+ try:
203
+ atomic_write(prov_path, data)
204
+ except OSError as exc:
205
+ return _fail(str(exc))
206
+ dropped = absent
207
+
208
+ result = {
209
+ "checked": checked,
210
+ "skipped": len(entries) - checked,
211
+ "absent": absent,
212
+ "dropped": dropped,
213
+ "provenance_written": bool(dropped),
214
+ }
215
+ # ASCII escapes: a Windows console's code page cannot encode every name.
216
+ sys.stdout.write(json.dumps(result, indent=2) + "\n")
217
+ return 0 if not absent or dropped else 1
218
+
219
+
220
+ if __name__ == "__main__":
221
+ sys.exit(main())