@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,787 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Manifest generator for Skill Graph (schema_version 4).
4
+ *
5
+ * Walks `skills/<name>/SKILL.md` (and optionally `examples/skill-metadata-template.md`),
6
+ * applies the authored-to-generated rename map documented in
7
+ * `docs/manifest-field-mapping.md`, computes summary aggregates, validates the
8
+ * result against `schemas/manifest.schema.json`, and emits the compiled
9
+ * `skills.manifest.json`.
10
+ *
11
+ * Workspace mode: when `.skill-graph/config.json` exists at the repo
12
+ * root and declares `workspace.skill_roots`, the generator walks every
13
+ * declared root instead of the default `skills/` directory. Each skill entry
14
+ * carries a `project` field identifying which root it came from. The manifest
15
+ * gains a top-level `workspace` block that echoes the config's projects map
16
+ * so consumers can resolve semantic tags without re-reading the config.
17
+ *
18
+ * Usage:
19
+ * node scripts/generate-manifest.js # emit to stdout
20
+ * node scripts/generate-manifest.js --output <path> # emit to file
21
+ * node scripts/generate-manifest.js --validate-only # validate, no output
22
+ * node scripts/generate-manifest.js --include-template # include examples/skill-metadata-template.md
23
+ * node scripts/generate-manifest.js --timestamp <ISO> # fixed timestamp for reproducible builds
24
+ *
25
+ * Self-contained. Only uses Node built-ins — no external dependencies.
26
+ * Exit 0 on success, 1 on validation failure.
27
+ */
28
+
29
+ 'use strict';
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const crypto = require('crypto');
34
+ const { parseFrontmatter, normalizeFrontmatter } = require('./lib/parse-frontmatter');
35
+ const { checkAliasParity } = require('./lib/alias-contract');
36
+ const { resolveSchemaPath, workspaceRoot } = require('./lib/roots');
37
+
38
+ const REPO_ROOT = workspaceRoot();
39
+ const DEFAULT_SKILLS_DIR = path.join(REPO_ROOT, 'skills');
40
+ const TEMPLATE_PATH = path.join(REPO_ROOT, 'examples', 'skill-metadata-template.md');
41
+ const MANIFEST_SCHEMA_PATH = resolveSchemaPath(REPO_ROOT, 'manifest.schema.json');
42
+ const CONFIG_PATH = path.join(REPO_ROOT, '.skill-graph', 'config.json');
43
+
44
+ function repoRelative(filePath) {
45
+ return path.relative(REPO_ROOT, filePath).split(path.sep).join('/');
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Workspace config (optional)
50
+ //
51
+ // Shape of `.skill-graph/config.json`:
52
+ // {
53
+ // "workspace": {
54
+ // "skill_roots": [
55
+ // { "path": "skills", "project": null },
56
+ // { "path": "<project-a>/.skill-graph/skills", "project": "<project-a>" }
57
+ // ],
58
+ // "projects": {
59
+ // "<project-a>": { "semantic_tags": ["ecommerce", "saas"] },
60
+ // "<project-b>": { "semantic_tags": ["ecommerce", "b2c"] }
61
+ // }
62
+ // }
63
+ // }
64
+ // (`<project-a>` / `<project-b>` are placeholders — replace with your actual
65
+ // project handles. Adopters declare their own; the OSS contract ships none.)
66
+ //
67
+ // When absent, the generator falls back to single-root mode with SKILLS_DIR
68
+ // and no project ownership.
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function loadWorkspaceConfig() {
72
+ if (!fs.existsSync(CONFIG_PATH)) return null;
73
+ try {
74
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
75
+ const config = JSON.parse(raw);
76
+ if (!config || typeof config !== 'object') return null;
77
+ if (!config.workspace || typeof config.workspace !== 'object') return null;
78
+ return config.workspace;
79
+ } catch (e) {
80
+ process.stderr.write(`WARN .skill-graph/config.json: cannot parse — ${e.message}\n`);
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Resolve the list of skill roots to walk.
87
+ *
88
+ * @param {object|null} workspace - Parsed workspace config (or null for single-root mode).
89
+ * @returns {Array<{absPath: string, project: string|null}>}
90
+ */
91
+ function resolveSkillRoots(workspace) {
92
+ if (!workspace || !Array.isArray(workspace.skill_roots) || workspace.skill_roots.length === 0) {
93
+ return [{ absPath: DEFAULT_SKILLS_DIR, project: null }];
94
+ }
95
+ return workspace.skill_roots
96
+ .map(entry => {
97
+ if (typeof entry === 'string') {
98
+ return { absPath: path.resolve(REPO_ROOT, entry), project: null };
99
+ }
100
+ if (entry && typeof entry === 'object' && typeof entry.path === 'string') {
101
+ return {
102
+ absPath: path.resolve(REPO_ROOT, entry.path),
103
+ project: (typeof entry.project === 'string' && entry.project.length > 0) ? entry.project : null,
104
+ };
105
+ }
106
+ return null;
107
+ })
108
+ .filter(Boolean);
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Truth source hashing (drift detection)
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Normalize legacy string truth sources and v3.1 anchored object truth sources
117
+ * into a stable key. The key is also the key used in
118
+ * drift_check.truth_source_hashes.
119
+ */
120
+ function normalizeTruthSource(src) {
121
+ if (typeof src === 'string') {
122
+ return { key: src, path: src, lineRange: null, anchor: null };
123
+ }
124
+ if (src && typeof src === 'object' && typeof src.path === 'string') {
125
+ const lineRange = src.line_range && typeof src.line_range === 'object'
126
+ ? {
127
+ start: Number.isInteger(src.line_range.start) ? src.line_range.start : null,
128
+ end: Number.isInteger(src.line_range.end) ? src.line_range.end : null,
129
+ }
130
+ : null;
131
+ const anchor = typeof src.anchor === 'string' && src.anchor.length > 0 ? src.anchor : null;
132
+ let key = src.path;
133
+ if (lineRange && lineRange.start) {
134
+ key += `#L${lineRange.start}-L${lineRange.end || lineRange.start}`;
135
+ } else if (anchor) {
136
+ key += `#${anchor}`;
137
+ }
138
+ return { key, path: src.path, lineRange, anchor };
139
+ }
140
+ return { key: String(src), path: null, lineRange: null, anchor: null };
141
+ }
142
+
143
+ function slugifyHeading(headingText) {
144
+ return headingText
145
+ .replace(/^#+\s*/, '')
146
+ .toLowerCase()
147
+ .replace(/[^a-z0-9]+/g, '-')
148
+ .replace(/-+/g, '-')
149
+ .replace(/^-|-$/g, '');
150
+ }
151
+
152
+ function isRemoteTruthSourcePath(value) {
153
+ return /^https?:\/\//i.test(String(value));
154
+ }
155
+
156
+ function sectionForHeadingAnchor(text, anchor) {
157
+ const lines = text.split('\n');
158
+ let start = -1;
159
+ let level = null;
160
+ for (let i = 0; i < lines.length; i++) {
161
+ const m = lines[i].match(/^(#{1,6})\s+(.+?)\s*$/);
162
+ if (m && slugifyHeading(m[2]) === anchor) {
163
+ start = i;
164
+ level = m[1].length;
165
+ break;
166
+ }
167
+ }
168
+ if (start === -1) return null;
169
+ let end = lines.length;
170
+ for (let i = start + 1; i < lines.length; i++) {
171
+ const m = lines[i].match(/^(#{1,6})\s+/);
172
+ if (m && m[1].length <= level) {
173
+ end = i;
174
+ break;
175
+ }
176
+ }
177
+ return lines.slice(start, end).join('\n');
178
+ }
179
+
180
+ /**
181
+ * Compute SHA-256 hex digest for a truth source. Legacy string entries hash the
182
+ * normalized whole file. Object entries with `line_range` hash only that
183
+ * inclusive line range, normalized to LF, which avoids CRLF-only drift.
184
+ */
185
+ function sha256TruthSource(src) {
186
+ const normalized = normalizeTruthSource(src);
187
+ if (!normalized.path) return null;
188
+ if (isRemoteTruthSourcePath(normalized.path)) return undefined;
189
+ try {
190
+ const abs = path.resolve(REPO_ROOT, normalized.path);
191
+ if (!fs.existsSync(abs)) return null;
192
+ const text = fs.readFileSync(abs, 'utf8').replace(/\r\n?/g, '\n');
193
+ let content = text;
194
+ if (normalized.lineRange && normalized.lineRange.start) {
195
+ const lines = text.split('\n');
196
+ const start = normalized.lineRange.start;
197
+ const end = normalized.lineRange.end || start;
198
+ if (start < 1 || end < start || end > lines.length) return null;
199
+ content = lines.slice(start - 1, end).join('\n');
200
+ } else if (normalized.anchor) {
201
+ const section = sectionForHeadingAnchor(text, normalized.anchor);
202
+ if (section !== null) content = section;
203
+ }
204
+ if (normalized.anchor && !text.includes(normalized.anchor) && sectionForHeadingAnchor(text, normalized.anchor) === null) return null;
205
+ return crypto.createHash('sha256').update(content).digest('hex');
206
+ } catch (e) {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Compare a skill's recorded `drift_check.truth_source_hashes` against the
213
+ * live file hashes. Returns true when any recorded hash differs from the
214
+ * current file hash, false otherwise. Returns null (unknown) when no hashes
215
+ * are recorded or no truth_sources are declared.
216
+ */
217
+ function detectDrift(fm) {
218
+ const recordedHashes = fm.drift_check && fm.drift_check.truth_source_hashes;
219
+ const truthSources = fm.grounding && fm.grounding.truth_sources;
220
+ if (!recordedHashes || typeof recordedHashes !== 'object') return null;
221
+ if (!Array.isArray(truthSources) || truthSources.length === 0) return null;
222
+
223
+ for (const src of truthSources) {
224
+ const normalized = normalizeTruthSource(src);
225
+ const recorded = recordedHashes[normalized.key];
226
+ if (!recorded) continue;
227
+ const live = sha256TruthSource(src);
228
+ if (live === undefined) continue; // URL truth source: valid but not hashable locally.
229
+ if (live === null) return true; // truth source vanished
230
+ if (live !== recorded) return true;
231
+ }
232
+ return false;
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Rename map — implements docs/manifest-field-mapping.md § "Top-level authored fields"
237
+ // ---------------------------------------------------------------------------
238
+
239
+ /**
240
+ * Apply the rename map to a parsed frontmatter object and a source file path,
241
+ * returning a validated manifest skill entry.
242
+ *
243
+ * @param {object} fm - Parsed frontmatter (from parseFrontmatter)
244
+ * @param {string} filePath - Absolute path to the source SKILL.md
245
+ * @param {string} skillId - Stable ID for this skill (directory name or derived)
246
+ * @param {string|null} project - Project handle for multi-root mode, or null for shared.
247
+ * @returns {object} Manifest skill entry object
248
+ */
249
+ function buildSkillEntry(fm, filePath, skillId, project) {
250
+ const aliasErrors = checkAliasParity(fm);
251
+ if (aliasErrors.length > 0) {
252
+ throw new Error(`alias contract violation: ${aliasErrors.join('; ')}`);
253
+ }
254
+
255
+ const entry = {};
256
+
257
+ // --- Generated fields ---
258
+ entry.id = skillId;
259
+ entry.path = repoRelative(filePath);
260
+ if (project) entry.project = project;
261
+
262
+ // --- Copied-through required fields ---
263
+ entry.name = fm.name;
264
+ if (fm.urn !== undefined && fm.urn !== null) {
265
+ entry.urn = fm.urn;
266
+ }
267
+ entry.description = fm.description;
268
+ entry.version = fm.version;
269
+ entry.type = fm.type;
270
+ if (fm.archetype !== undefined && fm.archetype !== null) {
271
+ entry.archetype = fm.archetype;
272
+ }
273
+ entry.category = fm.category;
274
+ entry.scope = fm.scope;
275
+ entry.owner = fm.owner;
276
+
277
+ // --- Copied-through optional fields ---
278
+ if (fm.domain !== undefined && fm.domain !== null) {
279
+ entry.domain = fm.domain;
280
+ }
281
+ if (fm.stability !== undefined && fm.stability !== null) {
282
+ entry.stability = fm.stability;
283
+ }
284
+ if (fm.superseded_by !== undefined && fm.superseded_by !== null) {
285
+ entry.superseded_by = fm.superseded_by;
286
+ }
287
+ if (fm.extends !== undefined && fm.extends !== null) {
288
+ entry.extends = fm.extends;
289
+ }
290
+ if (fm.license !== undefined && fm.license !== null) {
291
+ entry.license = fm.license;
292
+ }
293
+ if (fm.compatibility !== undefined && fm.compatibility !== null && typeof fm.compatibility === 'object') {
294
+ entry.compatibility = fm.compatibility;
295
+ }
296
+ if (fm['allowed-tools'] !== undefined && fm['allowed-tools'] !== null) {
297
+ entry['allowed-tools'] = fm['allowed-tools'];
298
+ }
299
+ if (fm.allowed_tools !== undefined && fm.allowed_tools !== null) {
300
+ entry.allowed_tools = fm.allowed_tools;
301
+ }
302
+ if (fm.routing_bundles !== undefined && fm.routing_bundles !== null) {
303
+ entry.routing_bundles = fm.routing_bundles;
304
+ }
305
+ if (Array.isArray(fm.workspace_tags) && fm.workspace_tags.length > 0) {
306
+ entry.workspace_tags = fm.workspace_tags;
307
+ }
308
+
309
+ // --- Grouped: activation (triggers + keywords + paths + examples + anti_examples) ---
310
+ const activation = {};
311
+ if (Array.isArray(fm.triggers) && fm.triggers.length > 0) {
312
+ activation.triggers = fm.triggers;
313
+ }
314
+ if (Array.isArray(fm.keywords) && fm.keywords.length > 0) {
315
+ activation.keywords = fm.keywords;
316
+ }
317
+ if (Array.isArray(fm.paths) && fm.paths.length > 0) {
318
+ activation.paths = fm.paths;
319
+ }
320
+ if (Array.isArray(fm.examples) && fm.examples.length > 0) {
321
+ activation.examples = fm.examples;
322
+ }
323
+ if (Array.isArray(fm.anti_examples) && fm.anti_examples.length > 0) {
324
+ activation.anti_examples = fm.anti_examples;
325
+ }
326
+ if (Object.keys(activation).length > 0) {
327
+ entry.activation = activation;
328
+ }
329
+
330
+ // --- Copied-through: relations (with v3 union-type items preserved as-is) ---
331
+ // Predicate set per ADR 0001 (v3.1 SKOS additions: related/broader/narrower) and ADR 0006
332
+ // (boundary stays canonical for routing-layer handoff; disjoint_with is a separate orthogonal
333
+ // relation for formal OWL class-disjointness). All seven keys flow through to the manifest;
334
+ // back-compat is preserved by keeping `adjacent` valid as an alias for `related`.
335
+ if (fm.relations !== null && fm.relations !== undefined && typeof fm.relations === 'object') {
336
+ const rel = {};
337
+ for (const kind of [
338
+ // v3.1 SKOS additions (preferred names; ADR 0001 Decisions #1 + #3)
339
+ 'related', 'broader', 'narrower',
340
+ // v3.0 stable + canonical (ADR 0006: boundary stays canonical)
341
+ 'adjacent', 'boundary', 'verify_with', 'depends_on',
342
+ // v3.1 separate orthogonal relation per ADR 0006 Option B
343
+ 'disjoint_with',
344
+ ]) {
345
+ if (Array.isArray(fm.relations[kind]) && fm.relations[kind].length > 0) {
346
+ rel[kind] = fm.relations[kind];
347
+ }
348
+ }
349
+ if (Object.keys(rel).length > 0) {
350
+ entry.relations = rel;
351
+ }
352
+ }
353
+
354
+ // --- Copied-through: grounding ---
355
+ if (fm.grounding !== null && fm.grounding !== undefined && typeof fm.grounding === 'object') {
356
+ entry.grounding = fm.grounding;
357
+ }
358
+
359
+ // --- Copied-through: portability ---
360
+ if (fm.portability !== null && fm.portability !== undefined && typeof fm.portability === 'object') {
361
+ entry.portability = fm.portability;
362
+ }
363
+
364
+ // --- Copied-through: concept teaching block ---
365
+ if (fm.concept !== null && fm.concept !== undefined && typeof fm.concept === 'object') {
366
+ entry.concept = fm.concept;
367
+ }
368
+
369
+ // --- Grouped: health (eval triple + freshness + drift_check + lifecycle + telemetry + generated booleans) ---
370
+ const health = {};
371
+ if (fm.eval_artifacts !== undefined && fm.eval_artifacts !== null) {
372
+ health.eval_artifacts = fm.eval_artifacts;
373
+ }
374
+ if (fm.eval_state !== undefined && fm.eval_state !== null) {
375
+ health.eval_state = fm.eval_state;
376
+ }
377
+ if (fm.routing_eval !== undefined && fm.routing_eval !== null) {
378
+ health.routing_eval = fm.routing_eval;
379
+ }
380
+ if (fm.comprehension_state !== undefined && fm.comprehension_state !== null) {
381
+ health.comprehension_state = fm.comprehension_state;
382
+ }
383
+ if (fm.eval_last_run !== undefined && fm.eval_last_run !== null && typeof fm.eval_last_run === 'object') {
384
+ health.eval_last_run = fm.eval_last_run;
385
+ }
386
+ if (fm.eval !== undefined && fm.eval !== null && typeof fm.eval === 'object') {
387
+ health.eval = fm.eval;
388
+ }
389
+ if (fm.freshness !== undefined && fm.freshness !== null) {
390
+ health.freshness = fm.freshness;
391
+ }
392
+ if (fm.reviewed_at !== undefined && fm.reviewed_at !== null) {
393
+ health.reviewed_at = fm.reviewed_at;
394
+ }
395
+ if (fm.drift_check !== undefined && fm.drift_check !== null && typeof fm.drift_check === 'object') {
396
+ health.drift_check = fm.drift_check;
397
+ }
398
+ if (fm.lifecycle !== undefined && fm.lifecycle !== null && typeof fm.lifecycle === 'object') {
399
+ health.lifecycle = fm.lifecycle;
400
+ }
401
+ if (fm.runtime_telemetry !== undefined && fm.runtime_telemetry !== null && typeof fm.runtime_telemetry === 'object') {
402
+ health.runtime_telemetry = fm.runtime_telemetry;
403
+ }
404
+ health.has_grounding = (entry.grounding !== undefined && entry.grounding !== null);
405
+ health.has_relations = (entry.relations !== undefined && Object.keys(entry.relations).length > 0);
406
+
407
+ // Drift detection (generated): compare truth_source_hashes against live files.
408
+ const drift = detectDrift(fm);
409
+ if (drift !== null) {
410
+ health.drift_detected = drift;
411
+ }
412
+
413
+ entry.health = health;
414
+
415
+ return entry;
416
+ }
417
+
418
+ /**
419
+ * Sort an object's keys deterministically (alphabetically).
420
+ * Arrays are preserved as-is (element order is authored order).
421
+ */
422
+ function sortKeys(value) {
423
+ if (Array.isArray(value)) return value.map(sortKeys);
424
+ if (value !== null && typeof value === 'object') {
425
+ const sorted = {};
426
+ for (const key of Object.keys(value).sort()) {
427
+ sorted[key] = sortKeys(value[key]);
428
+ }
429
+ return sorted;
430
+ }
431
+ return value;
432
+ }
433
+
434
+ /**
435
+ * Minimal JSON Schema validator covering the manifest schema shape.
436
+ * Validates type, required fields, enum, format (date, date-time), pattern,
437
+ * additionalProperties, and oneOf.
438
+ *
439
+ * Returns an array of error strings (empty = valid).
440
+ */
441
+ function validate(value, schema, pointer) {
442
+ if (pointer === undefined) pointer = '#';
443
+ const errors = [];
444
+
445
+ if (!schema || typeof schema !== 'object') return errors;
446
+
447
+ if (schema.type) {
448
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
449
+ const matchesType = (t) => {
450
+ if (t === 'null') return value === null;
451
+ if (t === 'array') return Array.isArray(value);
452
+ if (t === 'integer') return typeof value === 'number' && Number.isInteger(value);
453
+ if (t === 'number') return typeof value === 'number';
454
+ if (t === 'object') return typeof value === 'object' && value !== null && !Array.isArray(value);
455
+ return typeof value === t;
456
+ };
457
+ if (!types.some(matchesType)) {
458
+ const actualType =
459
+ value === null ? 'null' :
460
+ Array.isArray(value) ? 'array' :
461
+ typeof value;
462
+ errors.push(`${pointer}: expected type ${schema.type}, got ${actualType}`);
463
+ return errors;
464
+ }
465
+ }
466
+
467
+ if (schema.const !== undefined && value !== schema.const) {
468
+ errors.push(`${pointer}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
469
+ }
470
+
471
+ if (schema.enum && !schema.enum.includes(value)) {
472
+ errors.push(`${pointer}: value ${JSON.stringify(value)} not in enum [${schema.enum.map(e => JSON.stringify(e)).join(', ')}]`);
473
+ }
474
+
475
+ if (schema.pattern && typeof value === 'string') {
476
+ if (!new RegExp(schema.pattern).test(value)) {
477
+ errors.push(`${pointer}: "${value}" does not match pattern ${schema.pattern}`);
478
+ }
479
+ }
480
+
481
+ if (schema.format === 'date' && typeof value === 'string') {
482
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
483
+ errors.push(`${pointer}: "${value}" is not a valid date (expected YYYY-MM-DD)`);
484
+ }
485
+ }
486
+ if (schema.format === 'date-time' && typeof value === 'string') {
487
+ if (isNaN(Date.parse(value))) {
488
+ errors.push(`${pointer}: "${value}" is not a valid date-time`);
489
+ }
490
+ }
491
+
492
+ if (schema.minimum !== undefined && typeof value === 'number' && value < schema.minimum) {
493
+ errors.push(`${pointer}: ${value} < minimum ${schema.minimum}`);
494
+ }
495
+ if (schema.maximum !== undefined && typeof value === 'number' && value > schema.maximum) {
496
+ errors.push(`${pointer}: ${value} > maximum ${schema.maximum}`);
497
+ }
498
+
499
+ if (schema.maxLength !== undefined && typeof value === 'string' && value.length > schema.maxLength) {
500
+ errors.push(`${pointer}: length ${value.length} > maxLength ${schema.maxLength}`);
501
+ }
502
+
503
+ if (schema.oneOf) {
504
+ const matchCount = schema.oneOf.filter(sub => validate(value, sub, pointer + '/oneOf').length === 0).length;
505
+ if (matchCount !== 1) {
506
+ errors.push(`${pointer}: value does not match exactly one of the oneOf variants (matched ${matchCount})`);
507
+ }
508
+ }
509
+
510
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
511
+ const props = schema.properties || {};
512
+ const required = schema.required || [];
513
+
514
+ for (const req of required) {
515
+ if (!(req in value)) {
516
+ errors.push(`${pointer}/${req}: missing required field`);
517
+ }
518
+ }
519
+
520
+ if (schema.additionalProperties === false) {
521
+ for (const key of Object.keys(value)) {
522
+ if (!(key in props)) {
523
+ errors.push(`${pointer}/${key}: additional property not allowed`);
524
+ }
525
+ }
526
+ }
527
+
528
+ for (const [key, subValue] of Object.entries(value)) {
529
+ if (props[key]) {
530
+ const subErrors = validate(subValue, props[key], `${pointer}/${key}`);
531
+ errors.push(...subErrors);
532
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
533
+ const subErrors = validate(subValue, schema.additionalProperties, `${pointer}/${key}`);
534
+ errors.push(...subErrors);
535
+ }
536
+ }
537
+ }
538
+
539
+ if (Array.isArray(value)) {
540
+ if (schema.items) {
541
+ for (let i = 0; i < value.length; i++) {
542
+ const subErrors = validate(value[i], schema.items, `${pointer}/${i}`);
543
+ errors.push(...subErrors);
544
+ }
545
+ }
546
+ if (schema.minimum !== undefined && value.length < schema.minimum) {
547
+ errors.push(`${pointer}: array length ${value.length} < minimum ${schema.minimum}`);
548
+ }
549
+ }
550
+
551
+ return errors;
552
+ }
553
+
554
+ /**
555
+ * Collect skill source files to process.
556
+ *
557
+ * Walks every resolved skill root. When --include-template is passed, also
558
+ * includes `examples/skill-metadata-template.md` (marked as project=null).
559
+ */
560
+ function collectSources(args, skillRoots) {
561
+ const includeTemplate = args.includes('--include-template');
562
+ const sources = [];
563
+ const seen = new Set();
564
+
565
+ // Recursive walker (added 2026-05-18 for category folder structure).
566
+ // Skills now live at `<root>/<category>/[<domain>/]<name>/SKILL.md`,
567
+ // not the flat `<root>/<name>/SKILL.md`. Recurse up to 3 levels;
568
+ // stop descending once a SKILL.md is found in a directory.
569
+ function walkForSkillMd(dir, depth, project) {
570
+ if (depth > 3) return;
571
+ for (const name of fs.readdirSync(dir).sort()) {
572
+ if (name.startsWith('_') || name.startsWith('.')) continue;
573
+ const entry = path.join(dir, name);
574
+ if (!fs.statSync(entry).isDirectory()) continue;
575
+ const skillMd = path.join(entry, 'SKILL.md');
576
+ if (fs.existsSync(skillMd)) {
577
+ if (!seen.has(skillMd)) {
578
+ sources.push({ filePath: skillMd, skillId: name, project });
579
+ seen.add(skillMd);
580
+ }
581
+ } else {
582
+ walkForSkillMd(entry, depth + 1, project);
583
+ }
584
+ }
585
+ }
586
+
587
+ for (const { absPath, project } of skillRoots) {
588
+ if (!fs.existsSync(absPath)) continue;
589
+ const stat = fs.statSync(absPath);
590
+ if (!stat.isDirectory()) continue;
591
+ walkForSkillMd(absPath, 0, project);
592
+ }
593
+
594
+ if (includeTemplate && fs.existsSync(TEMPLATE_PATH) && !seen.has(TEMPLATE_PATH)) {
595
+ const text = fs.readFileSync(TEMPLATE_PATH, 'utf8');
596
+ const fm = normalizeFrontmatter(parseFrontmatter(text));
597
+ const id = (fm && fm.name) ? fm.name : 'skill-metadata-template';
598
+ sources.push({ filePath: TEMPLATE_PATH, skillId: id, project: null });
599
+ seen.add(TEMPLATE_PATH);
600
+ }
601
+
602
+ return sources;
603
+ }
604
+
605
+ /**
606
+ * Compute summary aggregates over the skills array.
607
+ */
608
+ function computeSummary(skills) {
609
+ const by_type = {};
610
+ const by_category = {};
611
+ const by_scope = {};
612
+ const by_stability = {};
613
+ const by_project = {};
614
+
615
+ for (const skill of skills) {
616
+ if (skill.type) by_type[skill.type] = (by_type[skill.type] || 0) + 1;
617
+ if (skill.category) by_category[skill.category] = (by_category[skill.category] || 0) + 1;
618
+ if (skill.scope) by_scope[skill.scope] = (by_scope[skill.scope] || 0) + 1;
619
+ if (skill.stability) by_stability[skill.stability] = (by_stability[skill.stability] || 0) + 1;
620
+ if (skill.project) by_project[skill.project] = (by_project[skill.project] || 0) + 1;
621
+ }
622
+
623
+ const summary = { total_skills: skills.length };
624
+ if (Object.keys(by_type).length > 0) summary.by_type = sortKeys(by_type);
625
+ if (Object.keys(by_category).length > 0) summary.by_category = sortKeys(by_category);
626
+ if (Object.keys(by_scope).length > 0) summary.by_scope = sortKeys(by_scope);
627
+ if (Object.keys(by_stability).length > 0) summary.by_stability = sortKeys(by_stability);
628
+ if (Object.keys(by_project).length > 0) summary.by_project = sortKeys(by_project);
629
+
630
+ return summary;
631
+ }
632
+
633
+ function main() {
634
+ const args = process.argv.slice(2);
635
+ const validateOnly = args.includes('--validate-only');
636
+ const outputPath = (() => {
637
+ const idx = args.indexOf('--output');
638
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
639
+ })();
640
+ const fixedTimestamp = (() => {
641
+ const idx = args.indexOf('--timestamp');
642
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : null;
643
+ })();
644
+
645
+ let manifestSchema;
646
+ try {
647
+ manifestSchema = JSON.parse(fs.readFileSync(MANIFEST_SCHEMA_PATH, 'utf8'));
648
+ } catch (e) {
649
+ console.error(`Error reading manifest schema: ${e.message}`);
650
+ process.exit(1);
651
+ }
652
+
653
+ // Resolve workspace (multi-root or single-root).
654
+ const workspace = loadWorkspaceConfig();
655
+ const skillRoots = resolveSkillRoots(workspace);
656
+
657
+ const sources = collectSources(args, skillRoots);
658
+ if (sources.length === 0) {
659
+ console.error('No skill files found. Check that the configured skill root(s) exist and contain SKILL.md files.');
660
+ process.exit(1);
661
+ }
662
+
663
+ const skillEntries = [];
664
+ const errors = [];
665
+
666
+ for (const { filePath, skillId, project } of sources) {
667
+ const relPath = repoRelative(filePath);
668
+ let text;
669
+ try {
670
+ text = fs.readFileSync(filePath, 'utf8');
671
+ } catch (e) {
672
+ errors.push(`${relPath}: cannot read file — ${e.message}`);
673
+ continue;
674
+ }
675
+
676
+ const fm = normalizeFrontmatter(parseFrontmatter(text));
677
+ if (!fm) {
678
+ errors.push(`${relPath}: no frontmatter found`);
679
+ continue;
680
+ }
681
+
682
+ // Legacy deprecation warnings.
683
+ if (fm.family) {
684
+ process.stderr.write(`WARN ${relPath}: "family" is deprecated — rename to "category"\n`);
685
+ }
686
+ if (fm.domain_frame) {
687
+ process.stderr.write(`WARN ${relPath}: "domain_frame" is deprecated — rename to "grounding"\n`);
688
+ }
689
+ if (fm.eval_status) {
690
+ process.stderr.write(`WARN ${relPath}: "eval_status" is deprecated — split into "eval_artifacts", "eval_state", and "routing_eval"\n`);
691
+ }
692
+ if (fm.route_groups) {
693
+ process.stderr.write(`WARN ${relPath}: "route_groups" is deprecated — rename to "routing_bundles"\n`);
694
+ }
695
+ if (typeof fm.drift_check === 'string') {
696
+ process.stderr.write(`WARN ${relPath}: scalar "drift_check" is deprecated in v3 — use an object with "last_verified" (run scripts/migrate-skill-v2-to-v3.js)\n`);
697
+ }
698
+ if (typeof fm.compatibility === 'string') {
699
+ process.stderr.write(`WARN ${relPath}: scalar "compatibility" is deprecated in v3 — use an object with "runtimes"/"node"/"notes" (run scripts/migrate-skill-v2-to-v3.js)\n`);
700
+ }
701
+
702
+ let entry;
703
+ try {
704
+ entry = buildSkillEntry(fm, filePath, skillId, project);
705
+ } catch (e) {
706
+ errors.push(`${relPath}: failed to build manifest entry — ${e.message}`);
707
+ continue;
708
+ }
709
+
710
+ skillEntries.push(entry);
711
+ }
712
+
713
+ if (errors.length > 0) {
714
+ for (const e of errors) console.error(`ERROR ${e}`);
715
+ process.exit(1);
716
+ }
717
+
718
+ skillEntries.sort((a, b) => a.id.localeCompare(b.id));
719
+ const sortedEntries = skillEntries.map(sortKeys);
720
+
721
+ // Build the manifest object.
722
+ const manifest = {
723
+ schema_version: 4,
724
+ generated_at: fixedTimestamp || new Date().toISOString(),
725
+ summary: computeSummary(skillEntries),
726
+ skills: sortedEntries,
727
+ };
728
+
729
+ // Emit workspace metadata block when a config is in effect.
730
+ if (workspace) {
731
+ const workspaceBlock = {};
732
+ if (Array.isArray(workspace.skill_roots)) {
733
+ workspaceBlock.skill_roots = workspace.skill_roots.map(e => typeof e === 'string' ? e : e.path);
734
+ }
735
+ if (workspace.projects && typeof workspace.projects === 'object') {
736
+ workspaceBlock.projects = workspace.projects;
737
+ }
738
+ if (Object.keys(workspaceBlock).length > 0) {
739
+ manifest.workspace = workspaceBlock;
740
+ }
741
+ }
742
+
743
+ const sortedManifest = sortKeys(manifest);
744
+
745
+ const validationErrors = validate(sortedManifest, manifestSchema);
746
+ if (validationErrors.length > 0) {
747
+ console.error('FAIL manifest validation:');
748
+ for (const e of validationErrors) console.error(` - ${e}`);
749
+ process.exit(1);
750
+ }
751
+
752
+ if (validateOnly) {
753
+ console.log(`OK manifest valid (${skillEntries.length} skill(s))`);
754
+ process.exit(0);
755
+ }
756
+
757
+ const json = JSON.stringify(sortedManifest, null, 2) + '\n';
758
+
759
+ if (outputPath) {
760
+ fs.writeFileSync(outputPath, json, 'utf8');
761
+ console.error(`OK manifest written to ${outputPath} (${skillEntries.length} skill(s))`);
762
+ process.exit(0);
763
+ } else {
764
+ // Wait for stdout to drain before exiting. Without this callback, process.exit()
765
+ // can terminate before the pipe buffer flushes when the manifest exceeds the OS
766
+ // pipe-buffer size (64 KB on macOS), causing silent truncation when the script
767
+ // is invoked via execFileSync/spawnSync (the harness used by skill-lint's
768
+ // generator-parity check). See `scripts/skill-lint.js` § "Generator parity".
769
+ process.stdout.write(json, () => process.exit(0));
770
+ }
771
+ }
772
+
773
+ if (require.main === module) {
774
+ main();
775
+ } else {
776
+ // Expose internals for testing without changing the CLI behaviour.
777
+ // The CLI still runs `main()` when invoked directly via `node generate-manifest.js`.
778
+ module.exports = {
779
+ buildSkillEntry,
780
+ sortKeys,
781
+ validate,
782
+ detectDrift,
783
+ normalizeTruthSource,
784
+ sha256TruthSource,
785
+ computeSummary,
786
+ };
787
+ }