@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,766 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * skill-graph route — the reference consumer for Skill Metadata Protocol metadata.
4
+ *
5
+ * Reads a compiled manifest (`skills.manifest.json` or
6
+ * `examples/skills.manifest.sample.json`) and picks skills for a natural-language
7
+ * query using every graph field that differentiates Skill Graph from plain
8
+ * SKILL.md:
9
+ *
10
+ * - activation.keywords / activation.triggers — scoring signal
11
+ * - activation.paths — optional `--path` boost
12
+ * - workspace_tags + workspace.projects — project-scope filter
13
+ * - relations.depends_on — transitive co-load
14
+ * - relations.boundary — anti-ownership exclusion
15
+ * - relations.verify_with — secondary co-load
16
+ * - health.eval_state — quality gate
17
+ * - health.drift_check + health.lifecycle — staleness annotation
18
+ *
19
+ * The output explains WHY each skill was selected or excluded. That is the
20
+ * point: the reference consumer exists so `boundary` and `depends_on` and
21
+ * `eval_state` are visible in a routing decision, not just declared in a
22
+ * frontmatter nobody reads.
23
+ *
24
+ * Usage:
25
+ * node scripts/skill-graph-route.js "accessibility keyboard navigation"
26
+ * node scripts/skill-graph-route.js "refactor" --project <your-project>
27
+ * node scripts/skill-graph-route.js "ssr hydration" --max 5
28
+ * node scripts/skill-graph-route.js "types" --min-eval-state passing
29
+ * node scripts/skill-graph-route.js "css" --path src/components/Header.tsx
30
+ * node scripts/skill-graph-route.js --manifest examples/skills.manifest.sample.json "css"
31
+ * node scripts/skill-graph-route.js --json "css"
32
+ *
33
+ * Self-contained. Only uses Node built-ins — no external dependencies.
34
+ * Exit 0 on success, 1 on manifest load failure or no query.
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const fs = require('fs');
40
+ const path = require('path');
41
+ const { packageRoot, workspaceRoot } = require('./lib/roots');
42
+
43
+ const REPO_ROOT = workspaceRoot();
44
+ const PACKAGE_ROOT = packageRoot();
45
+ const DEFAULT_MANIFEST = path.join(REPO_ROOT, 'skills.manifest.json');
46
+ const SAMPLE_MANIFEST = path.join(REPO_ROOT, 'examples', 'skills.manifest.sample.json');
47
+ const PACKAGE_SAMPLE_MANIFEST = path.join(PACKAGE_ROOT, 'examples', 'skills.manifest.sample.json');
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Tokenization & scoring
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * English function-word stopwords. Dropped from both query and keyword
55
+ * tokenization so that matches are carried by content tokens, not by
56
+ * pronouns / articles / auxiliaries / WH-words that appear in almost every
57
+ * natural-language prompt.
58
+ *
59
+ * Without this set, a query like "fix this" exact-matches any keyword phrase
60
+ * containing "this", driving library-wide false positives. See
61
+ * `docs/plans/routing-harness-followup.md` § M1.
62
+ */
63
+ /**
64
+ * Scope tiebreaker ranks (lower wins). Doctrine: a skill bound to a specific
65
+ * codebase is always more specific than a reference skill, which is always
66
+ * more specific than a portable skill. Used in `routeSkills()` sort. Unknown
67
+ * scopes fall back to `_default` so a manifest with a new scope value sorts
68
+ * last rather than throwing.
69
+ */
70
+ const SCOPE_RANK = { codebase: 0, reference: 1, portable: 2, _default: 99 };
71
+
72
+ /**
73
+ * Type tiebreaker ranks (lower wins). Doctrine: workflow > capability >
74
+ * router > overlay. Matches `skills/skill-router/SKILL.md § Type tiebreaker`.
75
+ */
76
+ const TYPE_RANK = { workflow: 0, capability: 1, router: 2, overlay: 3, _default: 99 };
77
+
78
+ const STOPWORDS = new Set([
79
+ 'a', 'an', 'the', 'this', 'that', 'these', 'those',
80
+ 'to', 'of', 'in', 'on', 'at', 'by', 'for', 'with', 'from', 'as', 'if',
81
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being',
82
+ 'it', 'its', 'my', 'our', 'your', 'their', 'his', 'her',
83
+ 'do', 'does', 'did', 'can', 'could', 'may', 'might', 'should', 'would',
84
+ 'have', 'has', 'had',
85
+ 'how', 'when', 'where', 'what', 'why', 'who', 'which',
86
+ 'and', 'or', 'not', 'but', 'so', 'then',
87
+ ]);
88
+
89
+ /**
90
+ * Split a query or keyword into lowercase content tokens.
91
+ *
92
+ * A token must be length ≥ 2 and NOT a stopword. The stopword filter applies
93
+ * equally on both sides of the comparison (query and keyword), so dropping a
94
+ * token from one side implicitly drops it from the other. The keyword side
95
+ * additionally enforces length ≥ 3 in `scoreSkill()` to prevent short-token
96
+ * false positives surviving stopword removal (e.g. "up", "it" in phrases
97
+ * like "set up the …").
98
+ */
99
+ function tokenize(text) {
100
+ return String(text || '')
101
+ .toLowerCase()
102
+ .split(/[^a-z0-9]+/)
103
+ .filter(t => t.length >= 2 && !STOPWORDS.has(t));
104
+ }
105
+
106
+ /**
107
+ * Score a skill's activation against a query. Higher is better.
108
+ *
109
+ * Signals, in decreasing weight:
110
+ * - trigger exact match: 5
111
+ * - keyword exact-token match: 3 (each query token credited at most ONCE
112
+ * per skill, regardless of how many keyword phrases contain it —
113
+ * prevents keyword-bag stuffing, see M2 in the follow-up plan)
114
+ * - keyword substring match: 1 (per distinct query token, also deduped
115
+ * against tokens already credited as exact matches)
116
+ * - path match on --path arg: 2
117
+ *
118
+ * Returns { score, matchedBecause } where matchedBecause is a short tag list
119
+ * used to explain the routing decision back to the user.
120
+ */
121
+ function scoreSkill(skill, queryTokens, pathArg) {
122
+ let score = 0;
123
+ const reasons = [];
124
+
125
+ const activation = skill.activation || {};
126
+ const triggers = activation.triggers || [];
127
+ const keywords = activation.keywords || [];
128
+ const paths = activation.paths || [];
129
+
130
+ // Triggers: exact match on the entire query OR on any word boundary.
131
+ const queryJoined = queryTokens.join(' ');
132
+ for (const trigger of triggers) {
133
+ const t = String(trigger).toLowerCase();
134
+ if (t === queryJoined || queryTokens.includes(t)) {
135
+ score += 5;
136
+ reasons.push(`trigger:${t}`);
137
+ }
138
+ }
139
+
140
+ // Keywords scored in TWO passes, each with its own per-token dedup set:
141
+ //
142
+ // Pass 1 (exact): each query token earns +3 AT MOST ONCE per skill,
143
+ // regardless of how many keyword phrases contain it.
144
+ // Prevents keyword-bag stuffing (M2 in the follow-up
145
+ // plan) — e.g. a skill with six phrases all containing
146
+ // "skill" can no longer beat a skill with two phrases
147
+ // but more precise content.
148
+ //
149
+ // Pass 2 (substring): each query token earns +1 AT MOST ONCE per skill,
150
+ // and ONLY if it did not already earn an exact match
151
+ // in pass 1. Running substring in a second pass —
152
+ // rather than falling back per-keyword inside pass 1
153
+ // — is the critical fix for the "cleanup" vs
154
+ // "clean this up" interaction. A substring credit on
155
+ // "clean" from the phrase "cleanup" must not poison a
156
+ // later exact match on "clean" from the phrase
157
+ // "clean this up". Separate sets keep the two dedups
158
+ // independent.
159
+ const exactMatchedTokens = new Set();
160
+ for (const keyword of keywords) {
161
+ const kwTokens = tokenize(keyword).filter(t => t.length >= 3);
162
+ for (const kw of kwTokens) {
163
+ if (queryTokens.includes(kw) && !exactMatchedTokens.has(kw)) {
164
+ exactMatchedTokens.add(kw);
165
+ score += 3;
166
+ reasons.push(`keyword:${keyword}`);
167
+ break;
168
+ }
169
+ }
170
+ }
171
+
172
+ const substringMatchedTokens = new Set();
173
+ for (const keyword of keywords) {
174
+ const full = String(keyword).toLowerCase();
175
+ for (const q of queryTokens) {
176
+ if (
177
+ q.length >= 3 &&
178
+ !exactMatchedTokens.has(q) &&
179
+ !substringMatchedTokens.has(q) &&
180
+ full.includes(q)
181
+ ) {
182
+ substringMatchedTokens.add(q);
183
+ score += 1;
184
+ reasons.push(`~keyword:${keyword}`);
185
+ break;
186
+ }
187
+ }
188
+ }
189
+
190
+ // Path match: if the caller passed --path, boost skills whose positive path
191
+ // list matches after gitignore-style negations have been applied.
192
+ if (pathArg) {
193
+ const pathMatch = matchPathList(pathArg, paths);
194
+ if (pathMatch.matched) {
195
+ score += 2;
196
+ reasons.push(`path:${pathMatch.pattern}`);
197
+ }
198
+ }
199
+
200
+ return { score, reasons };
201
+ }
202
+
203
+ /**
204
+ * Dependency-free glob support for activation.paths.
205
+ * Paths are matched against posix-style separators.
206
+ *
207
+ * `matchesGlob()` answers whether one concrete glob matches one path.
208
+ * `matchPathList()` applies authored list semantics, where `!pattern` only
209
+ * subtracts from prior positive includes.
210
+ */
211
+ function expandBraces(pattern) {
212
+ const match = String(pattern).match(/\{([^{}]+)\}/);
213
+ if (!match) return [String(pattern)];
214
+ const before = pattern.slice(0, match.index);
215
+ const after = pattern.slice(match.index + match[0].length);
216
+ return match[1].split(',').flatMap(part => expandBraces(before + part + after));
217
+ }
218
+
219
+ function globToRegExp(pattern) {
220
+ const pat = String(pattern).replace(/\\/g, '/');
221
+ let out = '^';
222
+ for (let i = 0; i < pat.length; i++) {
223
+ const ch = pat[i];
224
+ if (ch === '*') {
225
+ if (pat[i + 1] === '*') {
226
+ if (pat[i + 2] === '/') {
227
+ out += '(?:.*/)?';
228
+ i += 2;
229
+ } else {
230
+ out += '.*';
231
+ i += 1;
232
+ }
233
+ } else {
234
+ out += '[^/]*';
235
+ }
236
+ continue;
237
+ }
238
+ if (ch === '?') {
239
+ out += '[^/]';
240
+ continue;
241
+ }
242
+ out += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
243
+ }
244
+ return new RegExp(out + '$');
245
+ }
246
+
247
+ function matchesGlob(filePath, pattern) {
248
+ let pat = pattern;
249
+ if (typeof pat !== 'string' || pat.length === 0) return false;
250
+ if (pat.startsWith('!')) pat = pat.slice(1);
251
+ const normalizedPath = String(filePath).replace(/\\/g, '/');
252
+ return expandBraces(pat).some(expanded => globToRegExp(expanded).test(normalizedPath));
253
+ }
254
+
255
+ function matchPathList(filePath, patterns) {
256
+ let matched = false;
257
+ let matchedPattern = null;
258
+ let excludedBy = null;
259
+ if (!Array.isArray(patterns)) return { matched, pattern: null, excludedBy: null };
260
+
261
+ for (const pattern of patterns) {
262
+ if (typeof pattern !== 'string' || pattern.length === 0) continue;
263
+ const negated = pattern.startsWith('!');
264
+ if (!matchesGlob(filePath, pattern)) continue;
265
+ if (negated) {
266
+ if (matched) {
267
+ matched = false;
268
+ excludedBy = pattern;
269
+ }
270
+ } else {
271
+ matched = true;
272
+ matchedPattern = pattern;
273
+ excludedBy = null;
274
+ }
275
+ }
276
+
277
+ return { matched, pattern: matched ? matchedPattern : null, excludedBy };
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Relation helpers
282
+ // ---------------------------------------------------------------------------
283
+
284
+ /** Extract a skill name from a v2 bare-string or v3 {skill, ...} relation item. */
285
+ function relItemName(item) {
286
+ if (typeof item === 'string') return item;
287
+ if (item && typeof item === 'object' && typeof item.skill === 'string') return item.skill;
288
+ return null;
289
+ }
290
+
291
+ /** Extract a human-readable reason from a v3 boundary item, if present. */
292
+ function boundaryReason(item) {
293
+ if (item && typeof item === 'object' && typeof item.reason === 'string') return item.reason;
294
+ return null;
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Project-tag matching
299
+ // ---------------------------------------------------------------------------
300
+
301
+ /**
302
+ * Decide whether a skill applies to a given project handle, using the
303
+ * workspace config's semantic-tag mapping to expand the project into a set
304
+ * of matchable tags.
305
+ *
306
+ * A skill matches when:
307
+ * - it has no workspace_tags (ambient / cross-project), OR
308
+ * - any tag in workspace_tags matches the literal project handle, OR
309
+ * - any tag in workspace_tags matches one of the project's semantic_tags.
310
+ */
311
+ function skillAppliesToProject(skill, project, workspace) {
312
+ const tags = skill.workspace_tags || [];
313
+ if (tags.length === 0) return { applies: true, reason: 'ambient' };
314
+ if (!project) return { applies: true, reason: 'no project filter active' };
315
+
316
+ if (tags.includes(project)) return { applies: true, reason: `literal:${project}` };
317
+
318
+ const semanticTags = (workspace && workspace.projects && workspace.projects[project] && workspace.projects[project].semantic_tags) || [];
319
+ for (const tag of tags) {
320
+ if (semanticTags.includes(tag)) return { applies: true, reason: `semantic:${tag}` };
321
+ }
322
+ return { applies: false, reason: `workspace_tags [${tags.join(', ')}] exclude project "${project}"` };
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Staleness detection
327
+ // ---------------------------------------------------------------------------
328
+
329
+ function daysBetween(isoA, isoB) {
330
+ const a = new Date(isoA).getTime();
331
+ const b = new Date(isoB).getTime();
332
+ if (isNaN(a) || isNaN(b)) return null;
333
+ return Math.floor((b - a) / 86400000);
334
+ }
335
+
336
+ /**
337
+ * Compute staleness for a skill: { stale: boolean, days: number|null }.
338
+ *
339
+ * A skill is stale when lifecycle.stale_after_days is declared AND the days
340
+ * elapsed since drift_check.last_verified exceed that limit.
341
+ */
342
+ function computeStaleness(skill, today) {
343
+ const health = skill.health || {};
344
+ const lifecycle = (health.lifecycle) || {};
345
+ const driftCheck = health.drift_check;
346
+ if (!lifecycle.stale_after_days || !driftCheck || !driftCheck.last_verified) {
347
+ return { stale: false, days: null };
348
+ }
349
+ const days = daysBetween(driftCheck.last_verified, today);
350
+ if (days === null) return { stale: false, days: null };
351
+ return { stale: days > lifecycle.stale_after_days, days };
352
+ }
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // Routing pipeline
356
+ // ---------------------------------------------------------------------------
357
+
358
+ function routeSkills(manifest, options) {
359
+ const {
360
+ query,
361
+ project,
362
+ maxResults,
363
+ minEvalState,
364
+ pathArg,
365
+ todayISO,
366
+ } = options;
367
+
368
+ const skills = Array.isArray(manifest.skills) ? manifest.skills : [];
369
+ const workspace = manifest.workspace || null;
370
+ const queryTokens = tokenize(query);
371
+
372
+ const byName = new Map();
373
+ for (const s of skills) byName.set(s.name, s);
374
+
375
+ // -------------------------------------------------------------------------
376
+ // Stage 1: score every skill and filter by project.
377
+ // -------------------------------------------------------------------------
378
+ const scored = [];
379
+ const excludedByProject = [];
380
+
381
+ for (const skill of skills) {
382
+ const projectCheck = skillAppliesToProject(skill, project, workspace);
383
+ if (!projectCheck.applies) {
384
+ excludedByProject.push({ skill, reason: projectCheck.reason });
385
+ continue;
386
+ }
387
+ const { score, reasons } = scoreSkill(skill, queryTokens, pathArg);
388
+ if (score > 0) {
389
+ scored.push({ skill, score, reasons, role: 'match', projectMatch: projectCheck.reason });
390
+ }
391
+ }
392
+
393
+ if (scored.length === 0) {
394
+ return {
395
+ selected: [],
396
+ coLoaded: [],
397
+ excluded: [],
398
+ excludedByProject,
399
+ notes: ['no skills matched the query'],
400
+ };
401
+ }
402
+
403
+ // Tiebreakers implement the doctrine documented in
404
+ // `skills/skill-router/SKILL.md § Scope tiebreaker` and `§ Type tiebreaker`:
405
+ // 1. Highest score wins.
406
+ // 2. On a score tie, narrower scope wins: codebase > reference > portable.
407
+ // 3. On a scope tie, more specific type wins: workflow > capability > router > overlay.
408
+ // 4. On a complete tie, alphabetical by name (stable, deterministic output).
409
+ scored.sort((a, b) =>
410
+ (b.score - a.score) ||
411
+ (SCOPE_RANK[a.skill.scope] ?? SCOPE_RANK._default) - (SCOPE_RANK[b.skill.scope] ?? SCOPE_RANK._default) ||
412
+ (TYPE_RANK[a.skill.type] ?? TYPE_RANK._default) - (TYPE_RANK[b.skill.type] ?? TYPE_RANK._default) ||
413
+ a.skill.name.localeCompare(b.skill.name)
414
+ );
415
+
416
+ // -------------------------------------------------------------------------
417
+ // Stage 2: top-N matches seed the selection set.
418
+ // -------------------------------------------------------------------------
419
+ const topMatches = scored.slice(0, maxResults);
420
+ const selectedNames = new Set(topMatches.map(e => e.skill.name));
421
+
422
+ // -------------------------------------------------------------------------
423
+ // Stage 3: expand via depends_on transitive closure.
424
+ // -------------------------------------------------------------------------
425
+ const coLoaded = [];
426
+ const queue = topMatches.map(e => e.skill.name);
427
+ const visited = new Set(queue);
428
+
429
+ while (queue.length > 0) {
430
+ const current = queue.shift();
431
+ const skill = byName.get(current);
432
+ if (!skill || !skill.relations || !Array.isArray(skill.relations.depends_on)) continue;
433
+ for (const dep of skill.relations.depends_on) {
434
+ const depName = relItemName(dep);
435
+ if (!depName || visited.has(depName)) continue;
436
+ visited.add(depName);
437
+ const depSkill = byName.get(depName);
438
+ if (depSkill) {
439
+ const projectCheck = skillAppliesToProject(depSkill, project, workspace);
440
+ if (projectCheck.applies) {
441
+ coLoaded.push({
442
+ skill: depSkill,
443
+ reason: `depends_on closure from ${current}`,
444
+ role: 'depends_on',
445
+ });
446
+ selectedNames.add(depName);
447
+ queue.push(depName);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ // -------------------------------------------------------------------------
454
+ // Stage 4: verify_with co-loading (one hop only — no transitive).
455
+ // -------------------------------------------------------------------------
456
+ for (const { skill } of topMatches) {
457
+ if (!skill.relations || !Array.isArray(skill.relations.verify_with)) continue;
458
+ for (const v of skill.relations.verify_with) {
459
+ const vName = typeof v === 'string' ? v : null;
460
+ if (!vName || selectedNames.has(vName)) continue;
461
+ const vSkill = byName.get(vName);
462
+ if (vSkill) {
463
+ const projectCheck = skillAppliesToProject(vSkill, project, workspace);
464
+ if (projectCheck.applies) {
465
+ coLoaded.push({
466
+ skill: vSkill,
467
+ reason: `verify_with partner of ${skill.name}`,
468
+ role: 'verify_with',
469
+ });
470
+ selectedNames.add(vName);
471
+ }
472
+ }
473
+ }
474
+ }
475
+
476
+ // -------------------------------------------------------------------------
477
+ // Stage 4b: broader (SKOS generalisation) parent recall boost.
478
+ //
479
+ // Per ADR 0001 Decision #3, `relations.broader` declares cross-skill
480
+ // generalisation — the target is a more general skill the author wants
481
+ // co-loaded when the specific (child) skill matches. SKOS semantics:
482
+ // skos:broader(child, parent) means "parent is broader than child".
483
+ //
484
+ // Recall behaviour: when a topMatch declares `broader: [parent]`, the parent
485
+ // is co-loaded as a generalisation companion — the agent gets the broader
486
+ // context alongside the specific match. One hop only (no transitive
487
+ // ancestor walk), to mirror Stage 4's verify_with shape and avoid
488
+ // accidentally pulling in entire taxonomy chains.
489
+ //
490
+ // Inverse direction (`narrower`) is NOT co-loaded because if the parent
491
+ // matched, the children are NOT necessarily relevant — only an explicit
492
+ // child match should pull the parent in. (Authors who want parent →
493
+ // child co-loading should use `verify_with` or `depends_on`.)
494
+ // -------------------------------------------------------------------------
495
+ for (const { skill } of topMatches) {
496
+ if (!skill.relations || !Array.isArray(skill.relations.broader)) continue;
497
+ for (const b of skill.relations.broader) {
498
+ const bName = typeof b === 'string' ? b : null;
499
+ if (!bName || selectedNames.has(bName)) continue;
500
+ const bSkill = byName.get(bName);
501
+ if (bSkill) {
502
+ const projectCheck = skillAppliesToProject(bSkill, project, workspace);
503
+ if (projectCheck.applies) {
504
+ coLoaded.push({
505
+ skill: bSkill,
506
+ reason: `broader generalisation of ${skill.name}`,
507
+ role: 'broader',
508
+ });
509
+ selectedNames.add(bName);
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ // -------------------------------------------------------------------------
516
+ // Stage 5: score-aware boundary exclusion.
517
+ //
518
+ // A skill listed in another SELECTED skill's boundary[] is removed from
519
+ // the selection, subject to two guards:
520
+ //
521
+ // (1) Only skills that scored in stage 1 — i.e. topMatches — may act as
522
+ // declaring skills. Co-loaded skills (brought in via depends_on or
523
+ // verify_with, with no independent query score) MUST NOT boundary-
524
+ // exclude anyone. Otherwise a topMatch that pulls in a verify_with
525
+ // partner can be excluded BY THAT PARTNER if the partner's boundary
526
+ // names the topMatch — a cyclic invalidation of the topMatch's own
527
+ // win. The partner's boundary is authored as a request-time guard
528
+ // for when the partner itself is the primary match, not as a veto
529
+ // on whatever brought it along.
530
+ //
531
+ // (2) Even among topMatches, exclusion only fires if the declarer
532
+ // actually outscored the target. A weaker match cannot veto a
533
+ // stronger, more direct match on its own vocabulary (M3 in the
534
+ // follow-up plan). Score ties fall through to exclusion, so
535
+ // authored boundaries still break ties deterministically.
536
+ // -------------------------------------------------------------------------
537
+ const boundaryExcluded = [];
538
+ for (const declaring of topMatches) {
539
+ const skill = declaring.skill;
540
+ if (!skill.relations || !Array.isArray(skill.relations.boundary)) continue;
541
+ for (const b of skill.relations.boundary) {
542
+ const bName = relItemName(b);
543
+ const reason = boundaryReason(b);
544
+ if (!bName) continue;
545
+ if (!selectedNames.has(bName)) continue;
546
+
547
+ const bScored = scored.find(e => e.skill.name === bName);
548
+ if (bScored && bScored.score > declaring.score) {
549
+ // Target outscored the declarer on the query; keep it in selection.
550
+ continue;
551
+ }
552
+
553
+ const bSkill = byName.get(bName);
554
+ if (bSkill) {
555
+ boundaryExcluded.push({
556
+ skill: bSkill,
557
+ reason: reason
558
+ ? `in boundary[] of ${skill.name}: ${reason}`
559
+ : `in boundary[] of ${skill.name}`,
560
+ role: 'boundary_excluded',
561
+ });
562
+ selectedNames.delete(bName);
563
+ }
564
+ }
565
+ }
566
+
567
+ // -------------------------------------------------------------------------
568
+ // Stage 6: quality gate (eval_state).
569
+ // -------------------------------------------------------------------------
570
+ const gateRank = { unverified: 0, passing: 1, monitored: 2 };
571
+ const minGate = gateRank[minEvalState] ?? 0;
572
+ const qualityExcluded = [];
573
+
574
+ function passesGate(skill) {
575
+ const state = (skill.health && skill.health.eval_state) || 'unverified';
576
+ return (gateRank[state] ?? 0) >= minGate;
577
+ }
578
+
579
+ const filterGate = (list) => list.filter(entry => {
580
+ if (passesGate(entry.skill)) return true;
581
+ qualityExcluded.push({
582
+ skill: entry.skill,
583
+ reason: `eval_state=${(entry.skill.health && entry.skill.health.eval_state) || 'unverified'} below --min-eval-state=${minEvalState}`,
584
+ role: 'quality_excluded',
585
+ });
586
+ return false;
587
+ });
588
+
589
+ const selectedFiltered = filterGate(topMatches.filter(e => selectedNames.has(e.skill.name)));
590
+ const coLoadedFiltered = filterGate(coLoaded.filter(e => selectedNames.has(e.skill.name)));
591
+
592
+ // -------------------------------------------------------------------------
593
+ // Stage 7: staleness annotation (does not exclude).
594
+ // -------------------------------------------------------------------------
595
+ function annotate(entry) {
596
+ const s = computeStaleness(entry.skill, todayISO);
597
+ return { ...entry, staleness: s };
598
+ }
599
+
600
+ return {
601
+ selected: selectedFiltered.map(annotate),
602
+ coLoaded: coLoadedFiltered.map(annotate),
603
+ excluded: boundaryExcluded.concat(qualityExcluded).map(annotate),
604
+ excludedByProject,
605
+ notes: [],
606
+ };
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // Rendering
611
+ // ---------------------------------------------------------------------------
612
+
613
+ function pad(str, width) {
614
+ const s = String(str);
615
+ if (s.length >= width) return s.slice(0, width - 1) + '…';
616
+ return s + ' '.repeat(width - s.length);
617
+ }
618
+
619
+ function renderText(result, query) {
620
+ const lines = [];
621
+ lines.push(`Query: "${query}"`);
622
+ lines.push('');
623
+
624
+ if (result.notes.length > 0) {
625
+ for (const note of result.notes) lines.push(` (${note})`);
626
+ lines.push('');
627
+ }
628
+
629
+ const sec = (title, list, formatter) => {
630
+ if (list.length === 0) return;
631
+ lines.push(title);
632
+ lines.push(' ' + pad('Skill', 24) + pad('Score', 7) + pad('State', 12) + 'Reason');
633
+ lines.push(' ' + '─'.repeat(72));
634
+ for (const entry of list) lines.push(' ' + formatter(entry));
635
+ lines.push('');
636
+ };
637
+
638
+ sec('SELECTED', result.selected, e =>
639
+ pad(e.skill.name, 24) +
640
+ pad(String(e.score != null ? e.score : ''), 7) +
641
+ pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
642
+ (e.reasons ? e.reasons.join(', ') : '') +
643
+ (e.staleness && e.staleness.stale ? ` ⚠ stale (${e.staleness.days}d since last verify)` : '')
644
+ );
645
+
646
+ sec('CO-LOADED', result.coLoaded, e =>
647
+ pad(e.skill.name, 24) +
648
+ pad('—', 7) +
649
+ pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
650
+ e.reason +
651
+ (e.staleness && e.staleness.stale ? ` ⚠ stale (${e.staleness.days}d)` : '')
652
+ );
653
+
654
+ sec('EXCLUDED', result.excluded, e =>
655
+ pad(e.skill.name, 24) +
656
+ pad('—', 7) +
657
+ pad((e.skill.health && e.skill.health.eval_state) || '-', 12) +
658
+ e.reason
659
+ );
660
+
661
+ if (result.excludedByProject.length > 0) {
662
+ lines.push(`EXCLUDED BY PROJECT FILTER (${result.excludedByProject.length} skill(s) — not shown)`);
663
+ lines.push('');
664
+ }
665
+
666
+ const sCount = result.selected.length;
667
+ const cCount = result.coLoaded.length;
668
+ const xCount = result.excluded.length;
669
+ const staleCount = [...result.selected, ...result.coLoaded].filter(e => e.staleness && e.staleness.stale).length;
670
+ lines.push(`${sCount} selected, ${cCount} co-loaded, ${xCount} excluded. ${staleCount} stale.`);
671
+ return lines.join('\n');
672
+ }
673
+
674
+ function renderJson(result, query) {
675
+ // Trim the skill objects to a useful subset for programmatic consumers.
676
+ const trim = (e) => ({
677
+ name: e.skill.name,
678
+ id: e.skill.id,
679
+ path: e.skill.path,
680
+ score: e.score,
681
+ role: e.role,
682
+ eval_state: (e.skill.health && e.skill.health.eval_state) || null,
683
+ reasons: e.reasons,
684
+ reason: e.reason,
685
+ staleness: e.staleness,
686
+ });
687
+ return JSON.stringify({
688
+ query,
689
+ selected: result.selected.map(trim),
690
+ co_loaded: result.coLoaded.map(trim),
691
+ excluded: result.excluded.map(trim),
692
+ excluded_by_project: result.excludedByProject.map(e => ({ name: e.skill.name, reason: e.reason })),
693
+ }, null, 2);
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // CLI
698
+ // ---------------------------------------------------------------------------
699
+
700
+ function argValue(args, flag) {
701
+ const i = args.indexOf(flag);
702
+ return i !== -1 && args[i + 1] ? args[i + 1] : null;
703
+ }
704
+
705
+ function main() {
706
+ const args = process.argv.slice(2);
707
+ const outputJson = args.includes('--json');
708
+ const manifestArg = argValue(args, '--manifest');
709
+ const project = argValue(args, '--project') || process.env.SKILL_GRAPH_PROJECT || null;
710
+ const maxResults = parseInt(argValue(args, '--max') || '10', 10);
711
+ const minEvalState = argValue(args, '--min-eval-state') || 'unverified';
712
+ const pathArg = argValue(args, '--path');
713
+
714
+ // Everything that is not a flag and not a flag argument is treated as the query.
715
+ const nonFlag = [];
716
+ for (let i = 0; i < args.length; i++) {
717
+ const a = args[i];
718
+ if (a.startsWith('--')) {
719
+ if (['--manifest', '--project', '--max', '--min-eval-state', '--path'].includes(a)) i++;
720
+ continue;
721
+ }
722
+ nonFlag.push(a);
723
+ }
724
+ const query = nonFlag.join(' ').trim();
725
+
726
+ if (!query) {
727
+ console.error('Usage: skill-graph-route.js <query> [--project P] [--max N] [--min-eval-state unverified|passing|monitored] [--path FILE] [--manifest PATH] [--json]');
728
+ process.exit(1);
729
+ }
730
+
731
+ const manifestPath = manifestArg
732
+ ? path.resolve(manifestArg)
733
+ : (fs.existsSync(DEFAULT_MANIFEST)
734
+ ? DEFAULT_MANIFEST
735
+ : (fs.existsSync(SAMPLE_MANIFEST) ? SAMPLE_MANIFEST : PACKAGE_SAMPLE_MANIFEST));
736
+
737
+ if (!fs.existsSync(manifestPath)) {
738
+ console.error(`ERROR manifest not found: ${manifestPath}`);
739
+ console.error('Run `node scripts/generate-manifest.js --output skills.manifest.json` first, or pass --manifest <path>.');
740
+ process.exit(1);
741
+ }
742
+
743
+ let manifest;
744
+ try {
745
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
746
+ } catch (e) {
747
+ console.error(`ERROR cannot parse manifest: ${e.message}`);
748
+ process.exit(1);
749
+ }
750
+
751
+ const todayISO = new Date().toISOString().slice(0, 10);
752
+ const result = routeSkills(manifest, { query, project, maxResults, minEvalState, pathArg, todayISO });
753
+
754
+ if (outputJson) {
755
+ process.stdout.write(renderJson(result, query) + '\n');
756
+ } else {
757
+ process.stdout.write(renderText(result, query) + '\n');
758
+ }
759
+ process.exit(0);
760
+ }
761
+
762
+ // Allow require() from scripts/skill-graph-routing-eval.js so the harness
763
+ // can reuse the scoring + boundary-exclusion pipeline without shelling out.
764
+ module.exports = { routeSkills, tokenize, matchesGlob, matchPathList, computeStaleness };
765
+
766
+ if (require.main === module) main();