@skill-graph/cli 0.5.6

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 (330) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/LICENSE +200 -0
  3. package/NOTICE +62 -0
  4. package/README.md +398 -0
  5. package/SKILL_GRAPH.md +443 -0
  6. package/bin/skill-graph.js +374 -0
  7. package/docs/ADOPTION.md +117 -0
  8. package/docs/CONFORMANCE.md +66 -0
  9. package/docs/PRIMER.md +384 -0
  10. package/docs/QUICKSTART-30MIN.md +333 -0
  11. package/docs/ROUTING-METRICS.md +120 -0
  12. package/docs/SKILL-MD-FORMAT-COMPATIBILITY.md +127 -0
  13. package/docs/SKILL_AUDIT_CHECKLIST.md +199 -0
  14. package/docs/SKILL_AUDIT_LOOP.md +195 -0
  15. package/docs/SKILL_METADATA_PROTOCOL.md +609 -0
  16. package/docs/_archived/marketplace-publication-priority-2026-05-18.md +239 -0
  17. package/docs/adr/0001-predicate-set.md +69 -0
  18. package/docs/adr/0002-json-ld-context.md +82 -0
  19. package/docs/adr/0003-ontoclean-rigidity-tags.md +65 -0
  20. package/docs/adr/0004-persistent-identifiers.md +74 -0
  21. package/docs/adr/0005-freshness-consolidation.md +70 -0
  22. package/docs/adr/0006-revise-predicate-rename.md +105 -0
  23. package/docs/adr/0007-audit-loop-cadence.md +99 -0
  24. package/docs/adr/0008-skill-surface-split-and-curation-policy.md +93 -0
  25. package/docs/category-consumers.md +168 -0
  26. package/docs/concept-map.md +194 -0
  27. package/docs/diagrams/drift-states.mmd +21 -0
  28. package/docs/diagrams/manifest-pipeline.mmd +25 -0
  29. package/docs/diagrams/routing-harness.mmd +41 -0
  30. package/docs/diagrams/starter-graph.mmd +53 -0
  31. package/docs/field-decision-guide.md +315 -0
  32. package/docs/field-rationale.md +211 -0
  33. package/docs/field-reference.generated.md +624 -0
  34. package/docs/field-reference.md +1426 -0
  35. package/docs/glossary.md +190 -0
  36. package/docs/head-noun-glossary.md +63 -0
  37. package/docs/images/audit-phases.png +0 -0
  38. package/docs/images/drift-states.png +0 -0
  39. package/docs/images/graded-mode.png +0 -0
  40. package/docs/images/manifest-pipeline.png +0 -0
  41. package/docs/images/routing-harness.png +0 -0
  42. package/docs/images/skill-anatomy.png +0 -0
  43. package/docs/images/starter-graph.png +0 -0
  44. package/docs/images/system-model.png +0 -0
  45. package/docs/integrations/github-actions.md +155 -0
  46. package/docs/manifest-field-mapping.md +443 -0
  47. package/docs/marketplace-publication-queue.generated.md +240 -0
  48. package/docs/marketplace-release-agent-prompt.md +82 -0
  49. package/docs/marketplace-skill-candidate-list.md +272 -0
  50. package/docs/marketplace-syndication.md +222 -0
  51. package/docs/migration-sample-review.md +155 -0
  52. package/docs/migrations/v4-to-v5.md +168 -0
  53. package/docs/migrations/v5-to-v6.md +221 -0
  54. package/docs/name-exceptions.yaml +37 -0
  55. package/docs/plans/marketplace-p1-public-migration-plan.md +41 -0
  56. package/docs/plans/multi-root-workspace.md +148 -0
  57. package/docs/plans/scripts-roadmap.md +107 -0
  58. package/docs/plans/v4-schema-bump.md +160 -0
  59. package/docs/plans/wave-2-extraction.md +122 -0
  60. package/docs/positioning-vs-marketplaces.md +175 -0
  61. package/docs/proposals/skill-audit-loop-positioning.md +160 -0
  62. package/docs/quality-doctrine.md +138 -0
  63. package/docs/recommended-skills.md +150 -0
  64. package/docs/research/skill-comprehension-eval-research.md +1830 -0
  65. package/docs/research/skill-retrieval-evidence.md +66 -0
  66. package/docs/skill-metadata-protocol.md +471 -0
  67. package/docs/skills-sh-maintainer-cleanup-request.md +80 -0
  68. package/examples/audits/a11y/findings.md +52 -0
  69. package/examples/audits/a11y/scorecard.md +21 -0
  70. package/examples/audits/a11y/verdict.md +44 -0
  71. package/examples/audits/debugging/findings.md +59 -0
  72. package/examples/audits/debugging/scorecard.md +22 -0
  73. package/examples/audits/debugging/verdict.md +33 -0
  74. package/examples/audits/documentation/findings.md +59 -0
  75. package/examples/audits/documentation/scorecard.md +22 -0
  76. package/examples/audits/documentation/verdict.md +33 -0
  77. package/examples/evals/a11y.json +140 -0
  78. package/examples/evals/api-design.json +52 -0
  79. package/examples/evals/code-review.json +52 -0
  80. package/examples/evals/data-modeling.json +52 -0
  81. package/examples/evals/database-migration.json +52 -0
  82. package/examples/evals/debugging.json +118 -0
  83. package/examples/evals/dependency-architecture.json +52 -0
  84. package/examples/evals/design-system-architecture.json +52 -0
  85. package/examples/evals/error-tracking.json +52 -0
  86. package/examples/evals/event-contract-design.json +52 -0
  87. package/examples/evals/form-ux-architecture.json +52 -0
  88. package/examples/evals/framework-fit-analysis.json +52 -0
  89. package/examples/evals/graph-audit.json +139 -0
  90. package/examples/evals/information-architecture.json +52 -0
  91. package/examples/evals/interaction-feedback.json +52 -0
  92. package/examples/evals/interaction-patterns.json +52 -0
  93. package/examples/evals/layout-composition.json +52 -0
  94. package/examples/evals/lint-overlay.json +117 -0
  95. package/examples/evals/microcopy.json +52 -0
  96. package/examples/evals/observability-modeling.json +52 -0
  97. package/examples/evals/pattern-recognition.json +96 -0
  98. package/examples/evals/performance-engineering.json +52 -0
  99. package/examples/evals/refactor.json +128 -0
  100. package/examples/evals/semiotics.json +52 -0
  101. package/examples/evals/skill-infrastructure.json +96 -0
  102. package/examples/evals/skill-router.json +140 -0
  103. package/examples/evals/skill-router.routing.json +113 -0
  104. package/examples/evals/system-interface-contracts.json +52 -0
  105. package/examples/evals/task-analysis.json +52 -0
  106. package/examples/evals/testing-strategy.json +118 -0
  107. package/examples/evals/type-safety.json +249 -0
  108. package/examples/evals/visual-design-foundations.json +52 -0
  109. package/examples/evals/webhook-integration.json +52 -0
  110. package/examples/exports/a11y.skill-md.md +80 -0
  111. package/examples/exports/debugging.skill-md.md +80 -0
  112. package/examples/exports/refactor.skill-md.md +78 -0
  113. package/examples/exports/testing-strategy.skill-md.md +81 -0
  114. package/examples/projects/markdown-static-site/README.md +115 -0
  115. package/examples/projects/markdown-static-site/skills/content-source-router/SKILL.md +131 -0
  116. package/examples/projects/markdown-static-site/skills/image-optimization-pipeline-config/SKILL.md +132 -0
  117. package/examples/projects/markdown-static-site/skills/link-rot-detection/SKILL.md +103 -0
  118. package/examples/projects/markdown-static-site/skills/markdown-post-frontmatter-validation/SKILL.md +133 -0
  119. package/examples/projects/markdown-static-site/skills/migrate-posts-to-v2-frontmatter/SKILL.md +140 -0
  120. package/examples/projects/saas-stripe-postgres/README.md +208 -0
  121. package/examples/projects/saas-stripe-postgres/db/migrations/0004_canonicalize_orders.sql +37 -0
  122. package/examples/projects/saas-stripe-postgres/db/schema.sql +112 -0
  123. package/examples/projects/saas-stripe-postgres/skills/migrate-orders-to-canonical-schema/SKILL.md +149 -0
  124. package/examples/projects/saas-stripe-postgres/skills/nextjs-server-action-validation/SKILL.md +154 -0
  125. package/examples/projects/saas-stripe-postgres/skills/payment-provider-router/SKILL.md +153 -0
  126. package/examples/projects/saas-stripe-postgres/skills/postgres-rls-pattern/SKILL.md +163 -0
  127. package/examples/projects/saas-stripe-postgres/skills/stripe-webhook-signature-verification/SKILL.md +137 -0
  128. package/examples/protocol/skill-metadata-template.md +301 -0
  129. package/examples/protocol/skills.manifest.sample.json +13245 -0
  130. package/examples/skill-metadata-template.md +317 -0
  131. package/examples/skills.manifest.sample.json +13519 -0
  132. package/examples/tests/v3-1-skos-fixture/SKILL.md +93 -0
  133. package/marketplace/README.md +17 -0
  134. package/marketplace/skills/a11y/SKILL.md +66 -0
  135. package/marketplace/skills/acid-fundamentals/SKILL.md +106 -0
  136. package/marketplace/skills/agent-engineering/SKILL.md +386 -0
  137. package/marketplace/skills/agent-eval-design/SKILL.md +55 -0
  138. package/marketplace/skills/ai-native-development/SKILL.md +294 -0
  139. package/marketplace/skills/api-design/SKILL.md +60 -0
  140. package/marketplace/skills/architecture-decision-records/SKILL.md +55 -0
  141. package/marketplace/skills/background-jobs/SKILL.md +265 -0
  142. package/marketplace/skills/bounded-context-mapping/SKILL.md +55 -0
  143. package/marketplace/skills/cap-theorem-tradeoffs/SKILL.md +127 -0
  144. package/marketplace/skills/client-server-boundary/SKILL.md +187 -0
  145. package/marketplace/skills/code-review/SKILL.md +120 -0
  146. package/marketplace/skills/color-system-design/SKILL.md +43 -0
  147. package/marketplace/skills/component-architecture/SKILL.md +126 -0
  148. package/marketplace/skills/compression/SKILL.md +112 -0
  149. package/marketplace/skills/conceptual-modeling/SKILL.md +181 -0
  150. package/marketplace/skills/connection-pooling/SKILL.md +105 -0
  151. package/marketplace/skills/constraint-awareness/SKILL.md +287 -0
  152. package/marketplace/skills/content-monitor/SKILL.md +209 -0
  153. package/marketplace/skills/context-engineering/SKILL.md +320 -0
  154. package/marketplace/skills/context-graph/SKILL.md +174 -0
  155. package/marketplace/skills/context-management/SKILL.md +174 -0
  156. package/marketplace/skills/context-window/SKILL.md +239 -0
  157. package/marketplace/skills/contract-testing/SKILL.md +120 -0
  158. package/marketplace/skills/cron-scheduling/SKILL.md +223 -0
  159. package/marketplace/skills/dark-mode-implementation/SKILL.md +47 -0
  160. package/marketplace/skills/data-modeling/SKILL.md +59 -0
  161. package/marketplace/skills/data-modeling-fundamentals/SKILL.md +117 -0
  162. package/marketplace/skills/database-migration/SKILL.md +429 -0
  163. package/marketplace/skills/debugging/SKILL.md +67 -0
  164. package/marketplace/skills/dependency-architecture/SKILL.md +58 -0
  165. package/marketplace/skills/design-module-composition/SKILL.md +43 -0
  166. package/marketplace/skills/design-system-architecture/SKILL.md +61 -0
  167. package/marketplace/skills/design-thinking/SKILL.md +44 -0
  168. package/marketplace/skills/diagnosis/SKILL.md +296 -0
  169. package/marketplace/skills/diff-analysis/SKILL.md +188 -0
  170. package/marketplace/skills/e2e-test-design/SKILL.md +113 -0
  171. package/marketplace/skills/entity-relationship-modeling/SKILL.md +218 -0
  172. package/marketplace/skills/epistemic-grounding/SKILL.md +112 -0
  173. package/marketplace/skills/error-boundary/SKILL.md +235 -0
  174. package/marketplace/skills/error-tracking/SKILL.md +261 -0
  175. package/marketplace/skills/eval-driven-development/SKILL.md +147 -0
  176. package/marketplace/skills/evaluation/SKILL.md +113 -0
  177. package/marketplace/skills/event-contract-design/SKILL.md +60 -0
  178. package/marketplace/skills/event-storming/SKILL.md +56 -0
  179. package/marketplace/skills/form-ux-architecture/SKILL.md +60 -0
  180. package/marketplace/skills/framework-fit-analysis/SKILL.md +59 -0
  181. package/marketplace/skills/frontend-architecture/SKILL.md +43 -0
  182. package/marketplace/skills/generative-ui/SKILL.md +118 -0
  183. package/marketplace/skills/graph-audit/SKILL.md +81 -0
  184. package/marketplace/skills/guardrails/SKILL.md +118 -0
  185. package/marketplace/skills/hooks-patterns/SKILL.md +185 -0
  186. package/marketplace/skills/http-semantics/SKILL.md +136 -0
  187. package/marketplace/skills/ideation/SKILL.md +41 -0
  188. package/marketplace/skills/indexing-strategy/SKILL.md +108 -0
  189. package/marketplace/skills/information-architecture/SKILL.md +59 -0
  190. package/marketplace/skills/integration-test-design/SKILL.md +111 -0
  191. package/marketplace/skills/intent-recognition/SKILL.md +136 -0
  192. package/marketplace/skills/interaction-feedback/SKILL.md +59 -0
  193. package/marketplace/skills/interaction-patterns/SKILL.md +59 -0
  194. package/marketplace/skills/journey-mapping/SKILL.md +41 -0
  195. package/marketplace/skills/keywords/SKILL.md +213 -0
  196. package/marketplace/skills/knowledge-modeling/SKILL.md +232 -0
  197. package/marketplace/skills/layout-composition/SKILL.md +59 -0
  198. package/marketplace/skills/linguistics/SKILL.md +429 -0
  199. package/marketplace/skills/lint-overlay/SKILL.md +76 -0
  200. package/marketplace/skills/mental-models/SKILL.md +126 -0
  201. package/marketplace/skills/merge-queue/SKILL.md +94 -0
  202. package/marketplace/skills/methodology/SKILL.md +317 -0
  203. package/marketplace/skills/microcopy/SKILL.md +232 -0
  204. package/marketplace/skills/middleware-patterns/SKILL.md +363 -0
  205. package/marketplace/skills/mobile-responsive-ux/SKILL.md +287 -0
  206. package/marketplace/skills/mutation-testing/SKILL.md +112 -0
  207. package/marketplace/skills/naming-conventions/SKILL.md +112 -0
  208. package/marketplace/skills/observability-modeling/SKILL.md +59 -0
  209. package/marketplace/skills/ontology-modeling/SKILL.md +67 -0
  210. package/marketplace/skills/owasp-security/SKILL.md +153 -0
  211. package/marketplace/skills/pattern-recognition/SKILL.md +472 -0
  212. package/marketplace/skills/performance-budgets/SKILL.md +185 -0
  213. package/marketplace/skills/performance-engineering/SKILL.md +58 -0
  214. package/marketplace/skills/performance-testing/SKILL.md +125 -0
  215. package/marketplace/skills/printify/SKILL.md +42 -0
  216. package/marketplace/skills/prioritization/SKILL.md +118 -0
  217. package/marketplace/skills/problem-framing/SKILL.md +41 -0
  218. package/marketplace/skills/problem-locating-solving/SKILL.md +203 -0
  219. package/marketplace/skills/project-knowledge-extraction/SKILL.md +54 -0
  220. package/marketplace/skills/prompt-craft/SKILL.md +134 -0
  221. package/marketplace/skills/prompt-injection-defense/SKILL.md +132 -0
  222. package/marketplace/skills/property-based-testing/SKILL.md +100 -0
  223. package/marketplace/skills/prototyping/SKILL.md +43 -0
  224. package/marketplace/skills/query-optimization/SKILL.md +144 -0
  225. package/marketplace/skills/real-time-updates/SKILL.md +324 -0
  226. package/marketplace/skills/ref-patterns/SKILL.md +284 -0
  227. package/marketplace/skills/refactor/SKILL.md +65 -0
  228. package/marketplace/skills/rendering-models/SKILL.md +142 -0
  229. package/marketplace/skills/replication-patterns/SKILL.md +110 -0
  230. package/marketplace/skills/research-synthesis/SKILL.md +41 -0
  231. package/marketplace/skills/route-handler-design/SKILL.md +347 -0
  232. package/marketplace/skills/schema-evolution/SKILL.md +140 -0
  233. package/marketplace/skills/security-fundamentals/SKILL.md +139 -0
  234. package/marketplace/skills/semantic-center/SKILL.md +194 -0
  235. package/marketplace/skills/semantic-relations/SKILL.md +250 -0
  236. package/marketplace/skills/semantics/SKILL.md +366 -0
  237. package/marketplace/skills/semiotics/SKILL.md +230 -0
  238. package/marketplace/skills/seo-strategy/SKILL.md +260 -0
  239. package/marketplace/skills/server-actions-design/SKILL.md +243 -0
  240. package/marketplace/skills/server-components-design/SKILL.md +190 -0
  241. package/marketplace/skills/sharding-strategy/SKILL.md +123 -0
  242. package/marketplace/skills/shopify/SKILL.md +42 -0
  243. package/marketplace/skills/skill-infrastructure/SKILL.md +320 -0
  244. package/marketplace/skills/skill-router/SKILL.md +71 -0
  245. package/marketplace/skills/skill-scaffold/SKILL.md +105 -0
  246. package/marketplace/skills/snapshot-testing/SKILL.md +120 -0
  247. package/marketplace/skills/spec-driven-development/SKILL.md +148 -0
  248. package/marketplace/skills/state-machine-modeling/SKILL.md +56 -0
  249. package/marketplace/skills/state-management/SKILL.md +134 -0
  250. package/marketplace/skills/streaming-architecture/SKILL.md +194 -0
  251. package/marketplace/skills/summarization/SKILL.md +156 -0
  252. package/marketplace/skills/suspense-patterns/SKILL.md +265 -0
  253. package/marketplace/skills/system-interface-contracts/SKILL.md +59 -0
  254. package/marketplace/skills/task-analysis/SKILL.md +201 -0
  255. package/marketplace/skills/taxonomy-design/SKILL.md +66 -0
  256. package/marketplace/skills/test-coverage-strategy/SKILL.md +108 -0
  257. package/marketplace/skills/test-doubles-design/SKILL.md +98 -0
  258. package/marketplace/skills/test-driven-development/SKILL.md +96 -0
  259. package/marketplace/skills/testing-strategy/SKILL.md +67 -0
  260. package/marketplace/skills/theme-system-design/SKILL.md +43 -0
  261. package/marketplace/skills/tool-call-flow/SKILL.md +229 -0
  262. package/marketplace/skills/tool-call-strategy/SKILL.md +292 -0
  263. package/marketplace/skills/transaction-isolation/SKILL.md +98 -0
  264. package/marketplace/skills/type-safety/SKILL.md +177 -0
  265. package/marketplace/skills/typography-system/SKILL.md +43 -0
  266. package/marketplace/skills/usability-testing/SKILL.md +43 -0
  267. package/marketplace/skills/user-research/SKILL.md +43 -0
  268. package/marketplace/skills/vercel-composition-patterns/SKILL.md +157 -0
  269. package/marketplace/skills/version-control/SKILL.md +233 -0
  270. package/marketplace/skills/visual-design-foundations/SKILL.md +59 -0
  271. package/marketplace/skills/visual-hierarchy/SKILL.md +43 -0
  272. package/marketplace/skills/webhook-integration/SKILL.md +331 -0
  273. package/marketplace/skills/writing-humanizer/SKILL.md +380 -0
  274. package/package.json +67 -0
  275. package/schemas/manifest.schema.json +811 -0
  276. package/schemas/manifest.v2.schema.json +164 -0
  277. package/schemas/manifest.v3.schema.json +758 -0
  278. package/schemas/manifest.v4.schema.json +755 -0
  279. package/schemas/manifest.v5.schema.json +755 -0
  280. package/schemas/manifest.v6.schema.json +811 -0
  281. package/schemas/skill.context.jsonld +279 -0
  282. package/schemas/skill.schema.json +919 -0
  283. package/schemas/skill.v2.schema.json +201 -0
  284. package/schemas/skill.v3.schema.json +827 -0
  285. package/schemas/skill.v4.schema.json +822 -0
  286. package/schemas/skill.v5.schema.json +830 -0
  287. package/schemas/skill.v6.schema.json +946 -0
  288. package/schemas/vocabulary/keywords.json +180 -0
  289. package/schemas/vocabulary/workspace_tags.json +23 -0
  290. package/scripts/__tests__/migrate-skill-v2-to-v3.test.js +161 -0
  291. package/scripts/__tests__/migrate-skill-v3-to-v4.test.js +158 -0
  292. package/scripts/__tests__/test-export-parser-drift.js +149 -0
  293. package/scripts/__tests__/test-marketplace-export.js +114 -0
  294. package/scripts/__tests__/test-router-paths.js +82 -0
  295. package/scripts/__tests__/test-stability-promotion.js +244 -0
  296. package/scripts/__tests__/test-v3-1-alias-contract.js +109 -0
  297. package/scripts/__tests__/test-v3-1-skos-runtime.js +116 -0
  298. package/scripts/backfill-schema-version.js +198 -0
  299. package/scripts/build-field-reference.js +160 -0
  300. package/scripts/build-retrieval-baseline.js +511 -0
  301. package/scripts/check-markdown-links.js +211 -0
  302. package/scripts/check-protocol-consistency.js +979 -0
  303. package/scripts/export-marketplace-skills.js +610 -0
  304. package/scripts/export-skill.js +374 -0
  305. package/scripts/generate-manifest.js +787 -0
  306. package/scripts/lib/alias-contract.js +83 -0
  307. package/scripts/lib/audit-prompt-builder.js +771 -0
  308. package/scripts/lib/mock-grader.js +134 -0
  309. package/scripts/lib/parse-frontmatter.js +429 -0
  310. package/scripts/lib/roots.js +119 -0
  311. package/scripts/lint/check-archetype-sections.js +185 -0
  312. package/scripts/lint/check-category-enum.js +83 -0
  313. package/scripts/lint/check-routing-eval.js +146 -0
  314. package/scripts/lint/check-routing-quality.js +211 -0
  315. package/scripts/lint/check-stability-promotion.js +220 -0
  316. package/scripts/lint/format-code-frame.js +206 -0
  317. package/scripts/marketplace-install.js +125 -0
  318. package/scripts/migrate-category-to-enum.js +169 -0
  319. package/scripts/migrate-skill-v2-to-v3.js +424 -0
  320. package/scripts/migrate-skill-v3-to-v4.js +200 -0
  321. package/scripts/migrate-skill-v5-to-v6.js +304 -0
  322. package/scripts/restructure-by-category.js +85 -0
  323. package/scripts/seed-publication-classification.js +282 -0
  324. package/scripts/skill-audit.js +893 -0
  325. package/scripts/skill-graph-drift.js +483 -0
  326. package/scripts/skill-graph-route.js +766 -0
  327. package/scripts/skill-graph-routing-eval.js +393 -0
  328. package/scripts/skill-lint.js +1317 -0
  329. package/scripts/skill-overlap.js +213 -0
  330. package/scripts/verify-skill-md-export.js +201 -0
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Archetype-aware section validator for SKILL.md files.
4
+ *
5
+ * Each archetype has a minimum required set of H2 body sections defined in
6
+ * `docs/skill-metadata-protocol.md § Archetype Section Map`. This check:
7
+ *
8
+ * - Errors on any required H2 section that is missing from the body.
9
+ * - Warns on any H2 section that exists but whose content is empty
10
+ * (< 50 non-whitespace characters between consecutive H2 headers).
11
+ *
12
+ * The warning threshold (50 chars) catches placeholder sections like
13
+ * `## Verification\n\n(todo)\n` without tripping on intentionally terse
14
+ * but real sections such as a two-bullet `## Do NOT Use When` list.
15
+ *
16
+ * @module lint/check-archetype-sections
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ /**
22
+ * Required H2 sections per archetype, sourced from
23
+ * `docs/skill-metadata-protocol.md § Archetype Section Map`.
24
+ *
25
+ * @type {Record<string, string[]>}
26
+ */
27
+ const REQUIRED_SECTIONS = {
28
+ capability: ['Coverage', 'Philosophy', 'Verification', 'Do NOT Use When'],
29
+ workflow: ['Coverage', 'Philosophy', 'Workflow', 'Verification', 'Do NOT Use When'],
30
+ router: ['Coverage', 'Routing Rules', 'Do NOT Use When'],
31
+ overlay: ['Coverage', 'Overlay Rules', 'Extends', 'Do NOT Use When'],
32
+ };
33
+
34
+ /**
35
+ * Conditional section requirements. Applied on top of REQUIRED_SECTIONS based on
36
+ * frontmatter values. Each entry is `{ section, predicate, help }` — if the
37
+ * predicate returns true for the frontmatter, the section is required in
38
+ * addition to the archetype's base set.
39
+ *
40
+ * The `## Evals` rule closes a discoverability gap: a skill that declares
41
+ * `eval_artifacts: present` but has no `## Evals` body section hides the eval
42
+ * surface from readers (the artifact is findable only via the `examples/evals/`
43
+ * scan). Forcing the section guarantees the skill body links to its eval file.
44
+ */
45
+ const CONDITIONAL_SECTIONS = [
46
+ {
47
+ section: 'Evals',
48
+ predicate: (fm) => fm && fm.eval_artifacts === 'present',
49
+ help: 'Add a "## Evals" body section that references the eval artifact under examples/evals/ so readers can discover it from the SKILL.md alone.',
50
+ },
51
+ ];
52
+
53
+ /**
54
+ * Extract all top-level H2 section headers from the markdown body (the part
55
+ * after the closing `---` of the frontmatter block).
56
+ *
57
+ * @param {string} sourceText - Full SKILL.md content.
58
+ * @returns {Array<{heading: string, line: number, contentLength: number}>}
59
+ * Each entry has the heading text (without `## `), the 1-based line number
60
+ * of the `## …` line, and the number of non-whitespace characters in the
61
+ * section body (i.e. between this H2 and the next one or end-of-file).
62
+ */
63
+ function extractH2Sections(sourceText) {
64
+ const lines = sourceText.split(/\r?\n/);
65
+
66
+ // Find the end of the frontmatter block (second `---`).
67
+ let bodyStart = 0;
68
+ let dashCount = 0;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ if (lines[i].trim() === '---') {
71
+ dashCount++;
72
+ if (dashCount === 2) { bodyStart = i + 1; break; }
73
+ }
74
+ }
75
+
76
+ // Collect H2 positions in body.
77
+ const h2s = [];
78
+ for (let i = bodyStart; i < lines.length; i++) {
79
+ const m = lines[i].match(/^## (.+)$/);
80
+ if (m) {
81
+ h2s.push({ heading: m[1].trim(), lineIdx: i });
82
+ }
83
+ }
84
+
85
+ // For each H2, measure how many non-whitespace characters are in its body.
86
+ const sections = [];
87
+ for (let j = 0; j < h2s.length; j++) {
88
+ const start = h2s[j].lineIdx + 1;
89
+ const end = j + 1 < h2s.length ? h2s[j + 1].lineIdx : lines.length;
90
+ const body = lines.slice(start, end).join('\n');
91
+ sections.push({
92
+ heading: h2s[j].heading,
93
+ line: h2s[j].lineIdx + 1, // 1-based
94
+ contentLength: body.replace(/\s/g, '').length,
95
+ });
96
+ }
97
+
98
+ return sections;
99
+ }
100
+
101
+ /**
102
+ * Run archetype-aware section validation on one SKILL.md file.
103
+ *
104
+ * @param {object} opts
105
+ * @param {string} opts.filePath - Path to the file (used in messages).
106
+ * @param {string} opts.sourceText - Full file content.
107
+ * @param {object} opts.fm - Parsed frontmatter object.
108
+ * @param {number} [opts.emptyThreshold=50] - Minimum non-whitespace characters for
109
+ * a section to be considered non-empty.
110
+ *
111
+ * @returns {{
112
+ * errors: Array<{message: string, line: number, column: number, help: string}>,
113
+ * warnings: Array<{message: string, line: number, column: number, help: string}>
114
+ * }}
115
+ */
116
+ function checkArchetypeSections(opts) {
117
+ const {
118
+ filePath,
119
+ sourceText,
120
+ fm,
121
+ emptyThreshold = 50,
122
+ } = opts;
123
+
124
+ const errors = [];
125
+ const warnings = [];
126
+
127
+ if (!fm || !fm.type) {
128
+ // Cannot validate without a type — schema check handles missing type.
129
+ return { errors, warnings };
130
+ }
131
+
132
+ const archetype = fm.type;
133
+ const required = REQUIRED_SECTIONS[archetype];
134
+
135
+ if (!required) {
136
+ // Unknown archetype — schema check handles this.
137
+ return { errors, warnings };
138
+ }
139
+
140
+ const sections = extractH2Sections(sourceText);
141
+ const presentHeadings = new Set(sections.map(s => s.heading));
142
+
143
+ // Error: missing required sections.
144
+ for (const req of required) {
145
+ if (!presentHeadings.has(req)) {
146
+ // Point at line 1 col 1 (frontmatter type: field would be ideal but
147
+ // locateYamlKey is in the parent; use line 1 as a safe fallback — the
148
+ // formatter still shows the file reference clearly).
149
+ errors.push({
150
+ message: `missing required section "## ${req}" for archetype "${archetype}"`,
151
+ line: 1,
152
+ column: 1,
153
+ help: `Add a "## ${req}" section. Required sections for ${archetype}: ${required.map(r => `"## ${r}"`).join(', ')}. See docs/skill-metadata-protocol.md § Archetype Section Map.`,
154
+ });
155
+ }
156
+ }
157
+
158
+ // Conditional sections — required when frontmatter predicate holds.
159
+ for (const { section, predicate, help } of CONDITIONAL_SECTIONS) {
160
+ if (!predicate(fm)) continue;
161
+ if (presentHeadings.has(section)) continue;
162
+ errors.push({
163
+ message: `missing required section "## ${section}" (conditional: eval_artifacts is "${fm && fm.eval_artifacts}")`,
164
+ line: 1,
165
+ column: 1,
166
+ help,
167
+ });
168
+ }
169
+
170
+ // Warn: sections present but empty.
171
+ for (const section of sections) {
172
+ if (section.contentLength < emptyThreshold) {
173
+ warnings.push({
174
+ message: `section "## ${section.heading}" exists but appears empty (${section.contentLength} non-whitespace chars, threshold ${emptyThreshold})`,
175
+ line: section.line,
176
+ column: 1,
177
+ help: `Fill in the "## ${section.heading}" section or remove it if it is not needed for this archetype.`,
178
+ });
179
+ }
180
+ }
181
+
182
+ return { errors, warnings };
183
+ }
184
+
185
+ module.exports = { checkArchetypeSections, REQUIRED_SECTIONS };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Category enum check for SKILL.md files (Check 13).
4
+ *
5
+ * The Skill Graph policy is that `category` functions as a **browse facet**,
6
+ * not ontology truth, and must take exactly one of six canonical values.
7
+ *
8
+ * Post-Phase-1 (v5 schema bump, skill-graph commit `f489641`, 2026-05-16):
9
+ * the canonical enum is closed at BOTH the schema level (`schemas/skill.schema.json`
10
+ * `category.enum`) AND this lint level. This file is now redundant-but-correct:
11
+ * the schema will reject invalid values first, and this lint provides a
12
+ * second-layer guarantee with a more descriptive error message and the
13
+ * authoritative definitions inline.
14
+ *
15
+ * Both sources of truth must stay in sync. A future protocol revision that
16
+ * adds (e.g.) a 7th category must update three places in the same commit:
17
+ * 1. `schemas/skill.schema.json` `category.enum`
18
+ * 2. `CATEGORY_ENUM` below
19
+ * 3. `docs/skill-metadata-protocol.md` § Category
20
+ *
21
+ * @module lint/check-category-enum
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ /**
27
+ * Canonical category enum — closed set as of v5 (2026-05-16).
28
+ *
29
+ * Mirror of `schemas/skill.schema.json` `category.enum`. Update both in the
30
+ * same commit when adding/removing values.
31
+ *
32
+ * Framed as a BROWSE FACET, not ontology truth. Cross-cutting truth
33
+ * (a skill that is both engineering and quality, for example) lives in
34
+ * `relations.related`, not in multiple category memberships.
35
+ */
36
+ const CATEGORY_ENUM = Object.freeze([
37
+ 'foundations', // Epistemics, grounding, verification, context engineering, reasoning
38
+ 'engineering', // Building software systems
39
+ 'design', // Visual, interaction, IA, content, motion
40
+ 'quality', // Cross-cutting non-functional properties (a11y, perf, security, type-safety, testing)
41
+ 'agent', // Agent-specific concepts (tool design, prompt design, agent state)
42
+ 'product', // Prioritization, scope, MVP, PRDs, journeys
43
+ ]);
44
+
45
+ /**
46
+ * Run the category-enum check on one SKILL.md file.
47
+ *
48
+ * @param {object} opts
49
+ * @param {string} opts.filePath - Path to the file (used in messages only).
50
+ * @param {string} opts.sourceText - Full file content.
51
+ * @param {object} opts.fm - Parsed frontmatter object.
52
+ *
53
+ * @returns {{
54
+ * errors: Array<{message: string, line: number, column: number, help: string}>,
55
+ * warnings: Array<{message: string, line: number, column: number, help: string}>
56
+ * }}
57
+ */
58
+ function checkCategoryEnum(opts) {
59
+ const { sourceText, fm } = opts;
60
+ const errors = [];
61
+ const warnings = [];
62
+
63
+ if (!fm) return { errors, warnings };
64
+ if (typeof fm.category !== 'string') return { errors, warnings };
65
+
66
+ if (!CATEGORY_ENUM.includes(fm.category)) {
67
+ const lineMatch = sourceText.match(/^category\s*:.*$/m);
68
+ const lineNumber = lineMatch
69
+ ? sourceText.substring(0, sourceText.indexOf(lineMatch[0])).split('\n').length
70
+ : 1;
71
+
72
+ errors.push({
73
+ message: `category "${fm.category}" is not in the canonical enum: ${CATEGORY_ENUM.join(', ')}`,
74
+ line: lineNumber,
75
+ column: 1,
76
+ help: `Pick one of: ${CATEGORY_ENUM.join(', ')}. If the skill is genuinely cross-cutting, primary-category it under its strongest fit and use relations.related for the others. See docs/skill-metadata-protocol.md § Category and docs/plans/skill-taxonomy-v5-and-gap-fill.md.`,
77
+ });
78
+ }
79
+
80
+ return { errors, warnings };
81
+ }
82
+
83
+ module.exports = { checkCategoryEnum, CATEGORY_ENUM };
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Routing-eval integrity check for SKILL.md files (lint check 12).
4
+ *
5
+ * **R3 — routing_eval: present must be executable (ERROR)**
6
+ * A skill that declares `routing_eval: present` is claiming its routing
7
+ * coverage has been evaluated. The authored fields `examples` and
8
+ * `anti_examples` (schema_version 3, v0.5.0) are what the evaluation runs
9
+ * against. This check enforces two things:
10
+ *
11
+ * 1. If `routing_eval: present`, the skill MUST declare both `examples`
12
+ * and `anti_examples` (non-empty). No input — no evaluation possible.
13
+ *
14
+ * 2. If `routing_eval: present`, running the harness
15
+ * (`scripts/skill-graph-routing-eval.js`) against this skill MUST
16
+ * return verdict=PASS. Every FAIL case surfaces as a lint error with
17
+ * the failing prompt.
18
+ *
19
+ * This is the gate that turns `routing_eval` from self-assertion into a
20
+ * verifiable claim. Before this check, authors could set `present` without
21
+ * any executable evidence; after it, `present` is lint-rejected until the
22
+ * harness agrees.
23
+ *
24
+ * @module lint/check-routing-eval
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const { evaluateSkill } = require('../skill-graph-routing-eval');
32
+ const { packageRoot, workspaceRoot } = require('../lib/roots');
33
+
34
+ const REPO_ROOT = workspaceRoot();
35
+ const PACKAGE_ROOT = packageRoot();
36
+ const DEFAULT_MANIFEST = path.join(REPO_ROOT, 'skills.manifest.json');
37
+ const SAMPLE_MANIFEST = path.join(REPO_ROOT, 'examples', 'skills.manifest.sample.json');
38
+ const PACKAGE_SAMPLE_MANIFEST = path.join(PACKAGE_ROOT, 'examples', 'skills.manifest.sample.json');
39
+
40
+ /**
41
+ * Run routing-eval checks on one SKILL.md file.
42
+ *
43
+ * @param {object} opts
44
+ * @param {string} opts.filePath - Path to the file (used in messages only).
45
+ * @param {string} opts.sourceText - Full file content.
46
+ * @param {object} opts.fm - Parsed frontmatter object.
47
+ * @param {object} [opts.manifest] - Optional pre-loaded manifest (performance).
48
+ *
49
+ * @returns {{
50
+ * errors: Array<{message: string, line: number, column: number, help: string}>,
51
+ * warnings: Array<{message: string, line: number, column: number, help: string}>
52
+ * }}
53
+ */
54
+ function checkRoutingEval(opts) {
55
+ const { sourceText, fm } = opts;
56
+ const errors = [];
57
+ if (!fm) return { errors, warnings: [] };
58
+ if (fm.routing_eval !== 'present') return { errors, warnings: [] };
59
+
60
+ const keyLine = locateKey(sourceText, 'routing_eval') || { line: 1, column: 1 };
61
+
62
+ // Guard 1: examples + anti_examples must exist.
63
+ const hasExamples = Array.isArray(fm.examples) && fm.examples.length > 0;
64
+ const hasAnti = Array.isArray(fm.anti_examples) && fm.anti_examples.length > 0;
65
+ if (!hasExamples || !hasAnti) {
66
+ errors.push({
67
+ message: `routing_eval: present without populated examples + anti_examples — the harness has no prompts to evaluate`,
68
+ line: keyLine.line,
69
+ column: keyLine.column,
70
+ help: 'Either populate examples and anti_examples (see docs/field-reference.md § examples and § anti_examples) or set routing_eval to "absent".',
71
+ });
72
+ return { errors, warnings: [] };
73
+ }
74
+
75
+ // Guard 2: harness must pass.
76
+ const manifest = opts.manifest || loadManifest();
77
+ if (!manifest) {
78
+ errors.push({
79
+ message: `routing_eval: present but no manifest is available for the harness — run generate-manifest.js first`,
80
+ line: keyLine.line,
81
+ column: keyLine.column,
82
+ help: 'Run `node scripts/generate-manifest.js --output skills.manifest.json` to produce the manifest the harness evaluates against.',
83
+ });
84
+ return { errors, warnings: [] };
85
+ }
86
+
87
+ const skillEntry = (manifest.skills || []).find(s => s.name === fm.name);
88
+ if (!skillEntry) {
89
+ errors.push({
90
+ message: `routing_eval: present but skill "${fm.name}" is not in the manifest — regenerate the manifest`,
91
+ line: keyLine.line,
92
+ column: keyLine.column,
93
+ help: 'The skill must appear in the manifest so the harness can resolve its activation.examples / anti_examples.',
94
+ });
95
+ return { errors, warnings: [] };
96
+ }
97
+
98
+ const todayISO = new Date().toISOString().slice(0, 10);
99
+ const report = evaluateSkill(manifest, skillEntry, todayISO);
100
+
101
+ if (report.verdict === 'FAIL') {
102
+ for (const c of report.cases) {
103
+ if (c.verdict !== 'FAIL') continue;
104
+ errors.push({
105
+ message: `routing_eval: present but [${c.kind}] "${truncate(c.prompt, 72)}" fails — ${c.reason}`,
106
+ line: keyLine.line,
107
+ column: keyLine.column,
108
+ help: 'Either fix the routing signal (keywords / boundary / anti_example) until the harness passes, or set routing_eval to "absent" until you do. Run `node scripts/skill-graph-routing-eval.js --skill ' + fm.name + '` for full diagnostics.',
109
+ });
110
+ }
111
+ }
112
+
113
+ return { errors, warnings: [] };
114
+ }
115
+
116
+ function loadManifest() {
117
+ const p = fs.existsSync(DEFAULT_MANIFEST)
118
+ ? DEFAULT_MANIFEST
119
+ : (fs.existsSync(SAMPLE_MANIFEST) ? SAMPLE_MANIFEST : PACKAGE_SAMPLE_MANIFEST);
120
+ if (!fs.existsSync(p)) return null;
121
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); }
122
+ catch { return null; }
123
+ }
124
+
125
+ function locateKey(sourceText, key) {
126
+ const lines = sourceText.split('\n');
127
+ let dashCount = 0;
128
+ let inside = false;
129
+ for (let i = 0; i < lines.length; i++) {
130
+ if (lines[i].trim() === '---') {
131
+ dashCount++;
132
+ if (dashCount === 1) { inside = true; continue; }
133
+ if (dashCount === 2) break;
134
+ }
135
+ if (!inside) continue;
136
+ const m = lines[i].match(new RegExp(`^(\\s*)${key}\\s*:`));
137
+ if (m) return { line: i + 1, column: m[1].length + 1 };
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function truncate(s, n) {
143
+ return s.length <= n ? s : s.slice(0, n - 1) + '\u2026';
144
+ }
145
+
146
+ module.exports = { checkRoutingEval };
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Routing quality checks for SKILL.md files (A2 narrow).
4
+ *
5
+ * Only "defensible" heuristics are implemented here — checks with low false-
6
+ * positive risk and clear remediation paths. Length-threshold heuristics for
7
+ * description or coverage are intentionally omitted (high false-positive risk
8
+ * on legitimately terse skills).
9
+ *
10
+ * **Check R1 — empty keywords in operational scope (ERROR)**
11
+ * A skill with `scope: codebase` or any `routing_bundles` entry is meant to
12
+ * be surfaced by a router. An empty `keywords: []` (or no keywords field)
13
+ * means the skill cannot be discovered by keyword-based routers. This is
14
+ * almost always an authoring mistake.
15
+ *
16
+ * Note: the task spec text says "operational scope" but after SH-5784 the
17
+ * v2 scope value is `codebase`. This check targets `scope: codebase` OR
18
+ * skills that have a non-empty `routing_bundles` array, matching the intent
19
+ * of the original proposal.
20
+ *
21
+ * **Check R2 — description verbatim in Coverage section (WARN)**
22
+ * The description field is a routing contract (≤3 sentences) and `## Coverage`
23
+ * is a scope map (a bulleted topic list). When the description text appears
24
+ * verbatim inside `## Coverage`, the two layers have collapsed into one and
25
+ * the Coverage section adds no information for a reader who already read the
26
+ * frontmatter. This is always a copy-paste mistake.
27
+ *
28
+ * @module lint/check-routing-quality
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ /**
34
+ * Run routing-quality checks on one SKILL.md file.
35
+ *
36
+ * @param {object} opts
37
+ * @param {string} opts.filePath - Path to the file (used in messages only).
38
+ * @param {string} opts.sourceText - Full file content.
39
+ * @param {object} opts.fm - Parsed frontmatter object.
40
+ *
41
+ * @returns {{
42
+ * errors: Array<{message: string, line: number, column: number, help: string}>,
43
+ * warnings: Array<{message: string, line: number, column: number, help: string}>
44
+ * }}
45
+ */
46
+ function checkRoutingQuality(opts) {
47
+ const { filePath, sourceText, fm } = opts;
48
+
49
+ const errors = [];
50
+ const warnings = [];
51
+
52
+ if (!fm) return { errors, warnings };
53
+
54
+ // ------------------------------------------------------------------ R1
55
+ // Error: empty keywords for scope: codebase or routing_bundles-having skills.
56
+ const isCodebaseScope = fm.scope === 'codebase';
57
+ const hasRoutingGroups = Array.isArray(fm.routing_bundles) && fm.routing_bundles.length > 0;
58
+ // The custom frontmatter parser represents `keywords: []` as the string "[]"
59
+ // (inline YAML array syntax). Treat that as empty as well.
60
+ const keywordsEmpty = !fm.keywords
61
+ || fm.keywords === '[]'
62
+ || (Array.isArray(fm.keywords) && fm.keywords.length === 0);
63
+
64
+ if ((isCodebaseScope || hasRoutingGroups) && keywordsEmpty) {
65
+ // Locate the `keywords` key in the frontmatter if it exists, else point at
66
+ // the `scope` key.
67
+ const keyLine = locateKeyInFrontmatter(sourceText, 'keywords')
68
+ || locateKeyInFrontmatter(sourceText, 'scope')
69
+ || { line: 1, column: 1 };
70
+
71
+ const scopeReason = isCodebaseScope
72
+ ? 'scope: codebase'
73
+ : `routing_bundles: [${fm.routing_bundles.join(', ')}]`;
74
+
75
+ errors.push({
76
+ message: `keywords: [] for a skill with ${scopeReason} — router cannot discover this skill`,
77
+ line: keyLine.line,
78
+ column: keyLine.column,
79
+ help: 'Add at least one keyword to the keywords list. Skills with scope: codebase or routing_bundles must be discoverable by keyword routers. See docs/field-reference.md § keywords.',
80
+ });
81
+ }
82
+
83
+ // ------------------------------------------------------------------ R2
84
+ // Warn: description text appears verbatim inside ## Coverage section.
85
+ const description = typeof fm.description === 'string' ? fm.description.trim() : '';
86
+ if (description.length > 20) {
87
+ const coverageContent = extractSectionContent(sourceText, 'Coverage');
88
+ if (coverageContent !== null && coverageContent.includes(description)) {
89
+ const sectionLine = locateH2InBody(sourceText, 'Coverage');
90
+ warnings.push({
91
+ message: 'description text appears verbatim in ## Coverage — the two layers have collapsed into one',
92
+ line: sectionLine ? sectionLine.line : 1,
93
+ column: 1,
94
+ help: 'The description is the routing contract (≤3 sentences). ## Coverage is a bulleted scope map. Rewrite ## Coverage as a topic list, removing the copied description sentence. See docs/skill-metadata-protocol.md § Semantic layer discipline.',
95
+ });
96
+ }
97
+ }
98
+
99
+ return { errors, warnings };
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Internal helpers
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Locate a YAML key inside the frontmatter block.
108
+ * Returns { line, column } (1-based) or null if not found.
109
+ *
110
+ * @param {string} sourceText
111
+ * @param {string} key
112
+ * @returns {{ line: number, column: number }|null}
113
+ */
114
+ function locateKeyInFrontmatter(sourceText, key) {
115
+ const lines = sourceText.split('\n');
116
+ // Only search inside the frontmatter block (between the two `---` markers).
117
+ let inFrontmatter = false;
118
+ let dashCount = 0;
119
+ for (let i = 0; i < lines.length; i++) {
120
+ if (lines[i].trim() === '---') {
121
+ dashCount++;
122
+ if (dashCount === 1) { inFrontmatter = true; continue; }
123
+ if (dashCount === 2) break; // end of frontmatter
124
+ }
125
+ if (!inFrontmatter) continue;
126
+ const m = lines[i].match(new RegExp(`^(\\s*)${escapeRegex(key)}\\s*:`));
127
+ if (m) {
128
+ return { line: i + 1, column: m[1].length + 1 };
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Extract the text content of a named H2 section from the markdown body
136
+ * (after the closing `---`).
137
+ *
138
+ * @param {string} sourceText
139
+ * @param {string} heading - Heading text without `## ` prefix.
140
+ * @returns {string|null} Section content, or null if section not found.
141
+ */
142
+ function extractSectionContent(sourceText, heading) {
143
+ const lines = sourceText.split('\n');
144
+
145
+ // Skip past frontmatter.
146
+ let bodyStart = 0;
147
+ let dashCount = 0;
148
+ for (let i = 0; i < lines.length; i++) {
149
+ if (lines[i].trim() === '---') {
150
+ dashCount++;
151
+ if (dashCount === 2) { bodyStart = i + 1; break; }
152
+ }
153
+ }
154
+
155
+ const target = `## ${heading}`;
156
+ let collecting = false;
157
+ let content = [];
158
+
159
+ for (let i = bodyStart; i < lines.length; i++) {
160
+ if (lines[i].trimEnd() === target) {
161
+ collecting = true;
162
+ continue;
163
+ }
164
+ if (collecting) {
165
+ // Stop at the next H2.
166
+ if (/^## /.test(lines[i])) break;
167
+ content.push(lines[i]);
168
+ }
169
+ }
170
+
171
+ return collecting ? content.join('\n') : null;
172
+ }
173
+
174
+ /**
175
+ * Locate an H2 heading in the body (after frontmatter).
176
+ * Returns { line } (1-based) or null.
177
+ *
178
+ * @param {string} sourceText
179
+ * @param {string} heading
180
+ * @returns {{ line: number }|null}
181
+ */
182
+ function locateH2InBody(sourceText, heading) {
183
+ const lines = sourceText.split('\n');
184
+ let dashCount = 0;
185
+ let pastFrontmatter = false;
186
+ const target = `## ${heading}`;
187
+ for (let i = 0; i < lines.length; i++) {
188
+ if (!pastFrontmatter) {
189
+ if (lines[i].trim() === '---') {
190
+ dashCount++;
191
+ if (dashCount === 2) pastFrontmatter = true;
192
+ }
193
+ continue;
194
+ }
195
+ if (lines[i].trimEnd() === target) {
196
+ return { line: i + 1 };
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Escape special regex characters.
204
+ * @param {string} s
205
+ * @returns {string}
206
+ */
207
+ function escapeRegex(s) {
208
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
209
+ }
210
+
211
+ module.exports = { checkRoutingQuality };