@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,979 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Protocol consistency checker for Skill Graph.
4
+ *
5
+ * Runs 8 machine-detectable checks across the repo's protocol artifacts.
6
+ * These checks are complementary to skill-lint.js, which validates per-skill
7
+ * schema correctness. This script validates cross-artifact consistency:
8
+ * does the field reference doc match the schema? Does the manifest field mapping
9
+ * accurately describe what the generator does? Is the sample manifest correct?
10
+ *
11
+ * The 8 checks:
12
+ * C1 -- Field-set parity: docs/field-reference.md section headers vs
13
+ * schemas/skill.schema.json top-level properties.
14
+ * C2 -- Authored-to-generated parity: every skill.schema.json property either
15
+ * appears in manifest.schema.json (possibly grouped) or is listed as
16
+ * intentional loss in docs/manifest-field-mapping.md.
17
+ * C3 -- Artifact-root convention: detect when a shipped audit example (one that
18
+ * exists under examples/audits/) is referenced by a bare "audits/<skill>/"
19
+ * path in docs, conflicting with the canonical "examples/audits/<skill>/"
20
+ * root. Warns on conflicts; the two-tier consumer convention itself is
21
+ * intentional and is not flagged.
22
+ * C4 -- Sample manifest correctness: examples/skills.manifest.sample.json
23
+ * validates against schemas/manifest.schema.json AND summary.total_skills
24
+ * equals skills.length.
25
+ * C5 -- Example truth invariants:
26
+ * C5a: No worked scorecard claims 'exports cleanly to all' when portability
27
+ * targets are still aspirational (without a qualifying phrase).
28
+ * C5b: No eval artifact uses "eval_status" as a JSON key (deprecated v1
29
+ * field; post-SH-5784 use eval_artifacts, eval_state, routing_eval).
30
+ * C5c: Scorecard portability rows must not use v1 sub-field names
31
+ * ("level" or "exports") -- use v2 names "readiness" and "targets".
32
+ * C6 -- Versioned schema parity: schemas/skill.v2.schema.json and
33
+ * schemas/manifest.v2.schema.json must be content-identical to the
34
+ * unversioned schemas/skill.schema.json and schemas/manifest.schema.json,
35
+ * modulo $id and title. Drift between them breaks the pinning promise
36
+ * documented in docs/skill-metadata-protocol.md § Schema Versioning Policy.
37
+ * C7 -- Generated field-reference parity: docs/field-reference.generated.md
38
+ * must match live regeneration from the current pinned skill schema.
39
+ * C8 -- JSON-LD context coverage: every top-level authored schema field must
40
+ * appear in schemas/skill.context.jsonld and every compact IRI prefix
41
+ * used there must be declared.
42
+ *
43
+ * Usage:
44
+ * node scripts/check-protocol-consistency.js
45
+ * node scripts/check-protocol-consistency.js --verbose
46
+ *
47
+ * Self-contained. Only uses Node built-ins -- no external dependencies.
48
+ * Exit 0 on success, 1 on any check failure.
49
+ */
50
+
51
+ 'use strict';
52
+
53
+ const fs = require('fs');
54
+ const path = require('path');
55
+ const { parseFrontmatter } = require('./lib/parse-frontmatter');
56
+
57
+ const REPO_ROOT = path.resolve(__dirname, '..');
58
+ const VERBOSE = process.argv.includes('--verbose');
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function readJson(filePath) {
65
+ try {
66
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
67
+ } catch (e) {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ function readText(filePath) {
73
+ try {
74
+ return fs.readFileSync(filePath, 'utf8');
75
+ } catch (e) {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /** Extract all ## heading texts from a Markdown document. */
81
+ function extractH2Headings(text) {
82
+ const headings = [];
83
+ for (const line of text.split(/\r?\n/)) {
84
+ const m = line.match(/^##\s+(.+)$/);
85
+ if (m) headings.push(m[1].trim());
86
+ }
87
+ return headings;
88
+ }
89
+
90
+ /** Collect all .md files recursively under a directory. */
91
+ function collectMdFiles(dir) {
92
+ const results = [];
93
+ if (!fs.existsSync(dir)) return results;
94
+ for (const entry of fs.readdirSync(dir)) {
95
+ const full = path.join(dir, entry);
96
+ const stat = fs.statSync(full);
97
+ if (stat.isDirectory()) {
98
+ results.push(...collectMdFiles(full));
99
+ } else if (entry.endsWith('.md')) {
100
+ results.push(full);
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+
106
+ /** Minimal JSON Schema validator -- reused from generate-manifest.js. */
107
+ function validate(value, schema, pointer) {
108
+ if (pointer === undefined) pointer = '#';
109
+ const errors = [];
110
+ if (!schema || typeof schema !== 'object') return errors;
111
+
112
+ if (schema.type) {
113
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
114
+ const matchesType = (t) => {
115
+ if (t === 'null') return value === null;
116
+ if (t === 'array') return Array.isArray(value);
117
+ if (t === 'integer') return typeof value === 'number' && Number.isInteger(value);
118
+ if (t === 'number') return typeof value === 'number';
119
+ if (t === 'object') return typeof value === 'object' && value !== null && !Array.isArray(value);
120
+ return typeof value === t;
121
+ };
122
+ if (!types.some(matchesType)) {
123
+ const actualType = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
124
+ errors.push(`${pointer}: expected type ${schema.type}, got ${actualType}`);
125
+ return errors;
126
+ }
127
+ }
128
+
129
+ if (schema.const !== undefined && value !== schema.const) {
130
+ errors.push(`${pointer}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
131
+ }
132
+
133
+ if (schema.enum && !schema.enum.includes(value)) {
134
+ errors.push(`${pointer}: value ${JSON.stringify(value)} not in enum [${schema.enum.map(e => JSON.stringify(e)).join(', ')}]`);
135
+ }
136
+
137
+ if (schema.pattern && typeof value === 'string') {
138
+ if (!new RegExp(schema.pattern).test(value)) {
139
+ errors.push(`${pointer}: "${value}" does not match pattern ${schema.pattern}`);
140
+ }
141
+ }
142
+
143
+ if (schema.format === 'date' && typeof value === 'string') {
144
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
145
+ errors.push(`${pointer}: "${value}" is not a valid date (expected YYYY-MM-DD)`);
146
+ }
147
+ }
148
+ if (schema.format === 'date-time' && typeof value === 'string') {
149
+ if (isNaN(Date.parse(value))) {
150
+ errors.push(`${pointer}: "${value}" is not a valid date-time`);
151
+ }
152
+ }
153
+
154
+ if (schema.minimum !== undefined && typeof value === 'number' && value < schema.minimum) {
155
+ errors.push(`${pointer}: ${value} < minimum ${schema.minimum}`);
156
+ }
157
+
158
+ if (schema.oneOf) {
159
+ const matchCount = schema.oneOf.filter(sub => validate(value, sub, pointer + '/oneOf').length === 0).length;
160
+ if (matchCount !== 1) {
161
+ errors.push(`${pointer}: does not match exactly one of the oneOf variants (matched ${matchCount})`);
162
+ }
163
+ }
164
+
165
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
166
+ const props = schema.properties || {};
167
+ const required = schema.required || [];
168
+ for (const req of required) {
169
+ if (!(req in value)) {
170
+ errors.push(`${pointer}/${req}: missing required field`);
171
+ }
172
+ }
173
+ if (schema.additionalProperties === false) {
174
+ for (const key of Object.keys(value)) {
175
+ if (!(key in props)) {
176
+ errors.push(`${pointer}/${key}: additional property not allowed`);
177
+ }
178
+ }
179
+ }
180
+ for (const [key, subValue] of Object.entries(value)) {
181
+ if (props[key]) {
182
+ errors.push(...validate(subValue, props[key], `${pointer}/${key}`));
183
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
184
+ errors.push(...validate(subValue, schema.additionalProperties, `${pointer}/${key}`));
185
+ }
186
+ }
187
+ }
188
+
189
+ if (Array.isArray(value)) {
190
+ if (schema.items) {
191
+ for (let i = 0; i < value.length; i++) {
192
+ errors.push(...validate(value[i], schema.items, `${pointer}/${i}`));
193
+ }
194
+ }
195
+ }
196
+
197
+ return errors;
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Check C1 -- Field-set parity
202
+ //
203
+ // The ## headings in docs/field-reference.md are the authoritative list of
204
+ // authored fields. The top-level properties of schemas/skill.schema.json are
205
+ // the schema's field list. Both sets must be identical.
206
+ // ---------------------------------------------------------------------------
207
+
208
+ function checkC1FieldSetParity() {
209
+ const errors = [];
210
+ const refPath = path.join(REPO_ROOT, 'docs', 'field-reference.md');
211
+ const schemaPath = path.join(REPO_ROOT, 'schemas', 'skill.schema.json');
212
+
213
+ const refText = readText(refPath);
214
+ if (!refText) {
215
+ errors.push('C1 [docs/field-reference.md]: cannot read file -- field-set parity check skipped');
216
+ return errors;
217
+ }
218
+
219
+ const schema = readJson(schemaPath);
220
+ if (!schema || !schema.properties) {
221
+ errors.push('C1 [schemas/skill.schema.json]: cannot read schema or no "properties" key -- field-set parity check skipped');
222
+ return errors;
223
+ }
224
+
225
+ // Extract ## heading backtick-wrapped field names like ## `schema_version`
226
+ const headings = extractH2Headings(refText);
227
+ const docFields = new Set();
228
+ for (const h of headings) {
229
+ const m = h.match(/^`([^`]+)`$/);
230
+ if (m) docFields.add(m[1]);
231
+ }
232
+
233
+ const schemaFields = new Set(Object.keys(schema.properties));
234
+
235
+ const inSchemaNotDoc = [...schemaFields].filter(f => !docFields.has(f));
236
+ const inDocNotSchema = [...docFields].filter(f => !schemaFields.has(f));
237
+
238
+ if (inSchemaNotDoc.length > 0) {
239
+ errors.push(
240
+ `C1 [docs/field-reference.md vs schemas/skill.schema.json]: ` +
241
+ `${inSchemaNotDoc.length} schema field(s) not documented in field-reference.md: ` +
242
+ inSchemaNotDoc.map(f => `"${f}"`).join(', ')
243
+ );
244
+ }
245
+ if (inDocNotSchema.length > 0) {
246
+ errors.push(
247
+ `C1 [docs/field-reference.md vs schemas/skill.schema.json]: ` +
248
+ `${inDocNotSchema.length} field(s) documented in field-reference.md but absent from schema: ` +
249
+ inDocNotSchema.map(f => `"${f}"`).join(', ')
250
+ );
251
+ }
252
+
253
+ if (VERBOSE && errors.length === 0) {
254
+ console.log(` C1: OK -- ${docFields.size} fields documented, ${schemaFields.size} in schema, sets match`);
255
+ }
256
+
257
+ return errors;
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Check C2 -- Authored-to-generated parity
262
+ //
263
+ // Every property in schemas/skill.schema.json must either:
264
+ // (a) appear in schemas/manifest.schema.json (at top level or grouped under
265
+ // a parent like `activation`, `health`), OR
266
+ // (b) appear in the rename map table in docs/manifest-field-mapping.md, OR
267
+ // (c) be listed as intentional loss in the dropped-field list of
268
+ // docs/manifest-field-mapping.md.
269
+ //
270
+ // The rename map is parsed from the "Top-level authored fields" table.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ function checkC2AuthoredToGeneratedParity() {
274
+ const errors = [];
275
+ const skillSchemaPath = path.join(REPO_ROOT, 'schemas', 'skill.schema.json');
276
+ const manifestSchemaPath = path.join(REPO_ROOT, 'schemas', 'manifest.schema.json');
277
+ const mappingPath = path.join(REPO_ROOT, 'docs', 'manifest-field-mapping.md');
278
+
279
+ const skillSchema = readJson(skillSchemaPath);
280
+ if (!skillSchema || !skillSchema.properties) {
281
+ errors.push('C2 [schemas/skill.schema.json]: cannot read schema -- authored-to-generated parity check skipped');
282
+ return errors;
283
+ }
284
+
285
+ const manifestSchema = readJson(manifestSchemaPath);
286
+ if (!manifestSchema) {
287
+ errors.push('C2 [schemas/manifest.schema.json]: cannot read schema -- authored-to-generated parity check skipped');
288
+ return errors;
289
+ }
290
+
291
+ const mappingText = readText(mappingPath);
292
+ if (!mappingText) {
293
+ errors.push('C2 [docs/manifest-field-mapping.md]: cannot read file -- authored-to-generated parity check skipped');
294
+ return errors;
295
+ }
296
+
297
+ // Build the set of manifest field names -- top-level and grouped under skill items
298
+ const manifestFieldNames = new Set();
299
+
300
+ // From manifest schema skill item properties
301
+ const skillItemSchema =
302
+ manifestSchema.properties &&
303
+ manifestSchema.properties.skills &&
304
+ manifestSchema.properties.skills.items;
305
+ if (skillItemSchema && skillItemSchema.properties) {
306
+ for (const k of Object.keys(skillItemSchema.properties)) {
307
+ manifestFieldNames.add(k);
308
+ // Also collect sub-properties of grouped objects (activation, health, etc.)
309
+ const sub = skillItemSchema.properties[k];
310
+ if (sub && sub.properties) {
311
+ for (const sk of Object.keys(sub.properties)) {
312
+ manifestFieldNames.add(sk);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ // Parse the rename map table from manifest-field-mapping.md.
319
+ // Table rows look like: | N | `authored_field` | fate | manifest projection |
320
+ const renameMapFields = new Set();
321
+ const droppedFields = new Set();
322
+
323
+ let inRenameMap = false;
324
+ let inDroppedList = false;
325
+ for (const line of mappingText.split('\n')) {
326
+ if (/^##+ Top-level authored fields/.test(line)) {
327
+ inRenameMap = true;
328
+ inDroppedList = false;
329
+ continue;
330
+ }
331
+ if (/^##+ (Generated-only|Loss Policy|Migration|Worked Example|Verification)/.test(line)) {
332
+ inRenameMap = false;
333
+ }
334
+ if (/Current dropped-field list/.test(line)) {
335
+ inDroppedList = true;
336
+ continue;
337
+ }
338
+ if (inDroppedList && /^###/.test(line)) {
339
+ inDroppedList = false;
340
+ }
341
+
342
+ if (inRenameMap && line.startsWith('|')) {
343
+ // Column 2 (index 2 after splitting on '|') holds the authored field name
344
+ const cols = line.split('|').map(c => c.trim());
345
+ if (cols.length >= 3) {
346
+ const cell = cols[2];
347
+ const m = cell.match(/^`([^`]+)`/);
348
+ if (m) renameMapFields.add(m[1]);
349
+ }
350
+ }
351
+ if (inDroppedList && line.startsWith('|')) {
352
+ const cols = line.split('|').map(c => c.trim());
353
+ if (cols.length >= 2) {
354
+ const cell = cols[1];
355
+ const m = cell.match(/^`([^`]+)`/);
356
+ if (m && m[1] !== 'Field') droppedFields.add(m[1]);
357
+ }
358
+ }
359
+ }
360
+
361
+ // For each authored field, verify coverage
362
+ const authored = Object.keys(skillSchema.properties);
363
+ const uncovered = [];
364
+ for (const field of authored) {
365
+ const inManifest = manifestFieldNames.has(field);
366
+ const inRenameMapTable = renameMapFields.has(field);
367
+ const inDropped = droppedFields.has(field);
368
+ if (!inManifest && !inRenameMapTable && !inDropped) {
369
+ uncovered.push(field);
370
+ }
371
+ }
372
+
373
+ if (uncovered.length > 0) {
374
+ errors.push(
375
+ `C2 [schemas/skill.schema.json -> schemas/manifest.schema.json]: ` +
376
+ `${uncovered.length} authored field(s) not covered by manifest schema, rename map, or loss policy: ` +
377
+ uncovered.map(f => `"${f}"`).join(', ')
378
+ );
379
+ }
380
+
381
+ if (VERBOSE && errors.length === 0) {
382
+ console.log(
383
+ ` C2: OK -- ${authored.length} authored fields, all covered ` +
384
+ `(${manifestFieldNames.size} in manifest, ${renameMapFields.size} in rename map, ${droppedFields.size} dropped)`
385
+ );
386
+ }
387
+
388
+ return errors;
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // Check C3 -- Artifact-root convention
393
+ //
394
+ // Skill Graph uses a two-tier artifact root convention (documented in
395
+ // SKILL_AUDIT_LOOP.md after commit 873c463):
396
+ // - examples/audits/<skill>/ -- shipped, curated worked examples in this repo
397
+ // - audits/<skill>/ -- downstream consumer output (adopters' own repos)
398
+ //
399
+ // A conflict occurs when a doc describes a SHIPPED EXAMPLE living at the bare
400
+ // "audits/<skill>/" root instead of "examples/audits/<skill>/".
401
+ //
402
+ // We detect this by finding docs that reference a bare "audits/<skill>/" path
403
+ // for a skill that is known to have a shipped example under examples/audits/.
404
+ // The bare-path convention for consumer output is intentional and is NOT flagged.
405
+ //
406
+ // Excluded: docs/plans/ (describes script behavior, uses consumer paths by design)
407
+ // Lines that explicitly document the two-tier convention
408
+ // Fenced code blocks
409
+ //
410
+ // This is a WARN-level check -- it does not cause exit(1) by itself.
411
+ // ---------------------------------------------------------------------------
412
+
413
+ function checkC3ArtifactRootConvention() {
414
+ const warnings = [];
415
+
416
+ // Discover which skill names have shipped audit examples
417
+ const examplesAuditsDir = path.join(REPO_ROOT, 'examples', 'audits');
418
+ const shippedAuditSkills = new Set();
419
+ if (fs.existsSync(examplesAuditsDir)) {
420
+ for (const entry of fs.readdirSync(examplesAuditsDir)) {
421
+ const stat = fs.statSync(path.join(examplesAuditsDir, entry));
422
+ if (stat.isDirectory()) shippedAuditSkills.add(entry);
423
+ }
424
+ }
425
+
426
+ if (shippedAuditSkills.size === 0) {
427
+ if (VERBOSE) console.log(' C3: OK -- no shipped audit examples found in examples/audits/');
428
+ return warnings;
429
+ }
430
+
431
+ // Scan docs/ but exclude plans/ (those describe consumer script output paths)
432
+ const docsDir = path.join(REPO_ROOT, 'docs');
433
+ const mdFiles = collectMdFiles(docsDir).filter(f => {
434
+ const rel = path.relative(docsDir, f);
435
+ return !rel.startsWith('plans' + path.sep);
436
+ });
437
+
438
+ for (const filePath of mdFiles) {
439
+ const text = readText(filePath);
440
+ if (!text) continue;
441
+ const rel = path.relative(REPO_ROOT, filePath);
442
+
443
+ const lines = text.split('\n');
444
+ let inFencedBlock = false;
445
+ for (let i = 0; i < lines.length; i++) {
446
+ const line = lines[i];
447
+
448
+ if (line.trim().startsWith('```')) {
449
+ inFencedBlock = !inFencedBlock;
450
+ continue;
451
+ }
452
+ if (inFencedBlock) continue;
453
+
454
+ // Skip lines that explicitly document the two-tier convention
455
+ if (/two-tier|downstream consumer|adopter/i.test(line)) continue;
456
+
457
+ // Flag: bare "audits/<shipped-skill>/" not preceded by "examples/"
458
+ for (const skill of shippedAuditSkills) {
459
+ // Negative lookbehind: not preceded by "examples/"
460
+ const barePattern = new RegExp(`(?<!examples/)audits/${skill}/`);
461
+ if (barePattern.test(line)) {
462
+ warnings.push(
463
+ `C3 [${rel}:${i + 1}]: shipped audit example for skill "${skill}" referenced ` +
464
+ `as bare "audits/${skill}/" -- canonical shipped location is ` +
465
+ `"examples/audits/${skill}/". Line: ${line.trim().slice(0, 100)}`
466
+ );
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ if (VERBOSE && warnings.length === 0) {
473
+ console.log(' C3: OK -- no artifact-root conflicts found for shipped examples in docs/');
474
+ }
475
+
476
+ return warnings;
477
+ }
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // Check C4 -- Sample manifest correctness
481
+ //
482
+ // examples/skills.manifest.sample.json must:
483
+ // (a) validate against schemas/manifest.schema.json
484
+ // (b) have summary.total_skills === skills.length
485
+ // ---------------------------------------------------------------------------
486
+
487
+ function checkC4SampleManifestCorrectness() {
488
+ const errors = [];
489
+ const samplePath = path.join(REPO_ROOT, 'examples', 'skills.manifest.sample.json');
490
+ const schemaPath = path.join(REPO_ROOT, 'schemas', 'manifest.schema.json');
491
+
492
+ const sample = readJson(samplePath);
493
+ if (!sample) {
494
+ errors.push('C4 [examples/skills.manifest.sample.json]: cannot read file -- sample manifest correctness check skipped');
495
+ return errors;
496
+ }
497
+
498
+ const schema = readJson(schemaPath);
499
+ if (!schema) {
500
+ errors.push('C4 [schemas/manifest.schema.json]: cannot read schema -- sample manifest correctness check skipped');
501
+ return errors;
502
+ }
503
+
504
+ // (a) Validate against schema
505
+ const validationErrors = validate(sample, schema);
506
+ for (const e of validationErrors) {
507
+ errors.push(`C4 [examples/skills.manifest.sample.json]: schema validation failed -- ${e}`);
508
+ }
509
+
510
+ // (b) summary.total_skills === skills.length
511
+ if (typeof sample.summary === 'object' && sample.summary !== null && Array.isArray(sample.skills)) {
512
+ const declared = sample.summary.total_skills;
513
+ const actual = sample.skills.length;
514
+ if (declared !== actual) {
515
+ errors.push(
516
+ `C4 [examples/skills.manifest.sample.json]: ` +
517
+ `summary.total_skills (${declared}) does not equal skills.length (${actual})`
518
+ );
519
+ }
520
+ } else {
521
+ if (!Array.isArray(sample.skills)) {
522
+ errors.push('C4 [examples/skills.manifest.sample.json]: missing or non-array "skills" field');
523
+ }
524
+ if (typeof sample.summary !== 'object' || sample.summary === null) {
525
+ errors.push('C4 [examples/skills.manifest.sample.json]: missing or non-object "summary" field');
526
+ }
527
+ }
528
+
529
+ if (VERBOSE && errors.length === 0) {
530
+ console.log(
531
+ ` C4: OK -- sample manifest validates against schema; ` +
532
+ `total_skills=${sample.summary.total_skills} matches skills.length=${sample.skills.length}`
533
+ );
534
+ }
535
+
536
+ return errors;
537
+ }
538
+
539
+ // ---------------------------------------------------------------------------
540
+ // Check C5 -- Example truth invariants
541
+ //
542
+ // C5a: No worked scorecard claims "exports cleanly to all" without qualifying
543
+ // that some portability targets are still aspirational. The pre-873c463
544
+ // scorecard said "exports cleanly to all four" (unqualified). The repair
545
+ // added a qualifier. We flag any scorecard that restores the bare form.
546
+ //
547
+ // C5b: No eval artifact uses "eval_status" as a JSON metadata key. This is the
548
+ // deprecated v1 field; post-SH-5784 the correct keys are eval_artifacts,
549
+ // eval_state, and routing_eval. We check JSON object keys -- not prompt text
550
+ // (prompt text may discuss the old field name for educational purposes).
551
+ //
552
+ // C5c: Scorecard portability rows must not use v1 sub-field names ("level" or
553
+ // "exports") -- use v2 names "readiness" and "targets" instead.
554
+ // ---------------------------------------------------------------------------
555
+
556
+ function checkC5ExampleTruthInvariants() {
557
+ const errors = [];
558
+ const auditsDir = path.join(REPO_ROOT, 'examples', 'audits');
559
+ const evalsDir = path.join(REPO_ROOT, 'examples', 'evals');
560
+
561
+ // C5a + C5c -- Scorecard checks
562
+ const scorecardFiles = collectMdFiles(auditsDir).filter(f => f.endsWith('scorecard.md'));
563
+ for (const filePath of scorecardFiles) {
564
+ const text = readText(filePath);
565
+ if (!text) continue;
566
+ const rel = path.relative(REPO_ROOT, filePath);
567
+
568
+ const lines = text.split('\n');
569
+ for (let i = 0; i < lines.length; i++) {
570
+ const line = lines[i];
571
+
572
+ // C5a: "exports cleanly to all" without aspirational qualifier nearby
573
+ if (/exports cleanly to all/i.test(line)) {
574
+ const window = lines.slice(Math.max(0, i - 1), i + 3).join(' ');
575
+ if (!/aspirational|still planned|not yet|pending/i.test(window)) {
576
+ errors.push(
577
+ `C5a [${rel}:${i + 1}]: scorecard claims "exports cleanly to all" without qualifying ` +
578
+ `that some portability targets are still aspirational. ` +
579
+ `Add a qualifier (e.g. "cursor, windsurf, and copilot are still aspirational"). ` +
580
+ `Line: ${line.trim().slice(0, 100)}`
581
+ );
582
+ }
583
+ }
584
+
585
+ // C5c: v1 portability sub-field names
586
+ if (/portability\.level|portability\.exports|\blevel:\s*(high|medium|low)\b/i.test(line)) {
587
+ errors.push(
588
+ `C5c [${rel}:${i + 1}]: scorecard uses v1 portability field names ` +
589
+ `("level" or "exports") -- use schema_version 2 names "readiness" and "targets". ` +
590
+ `Line: ${line.trim().slice(0, 100)}`
591
+ );
592
+ }
593
+ }
594
+ }
595
+
596
+ // C5b -- Eval artifact JSON key checks
597
+ const evalFiles = [];
598
+ if (fs.existsSync(evalsDir)) {
599
+ for (const entry of fs.readdirSync(evalsDir)) {
600
+ if (entry.endsWith('.json')) {
601
+ evalFiles.push(path.join(evalsDir, entry));
602
+ }
603
+ }
604
+ }
605
+
606
+ for (const filePath of evalFiles) {
607
+ const text = readText(filePath);
608
+ if (!text) continue;
609
+ const rel = path.relative(REPO_ROOT, filePath);
610
+
611
+ // Parse JSON and inspect object keys recursively.
612
+ // We check keys, not string values -- prompt text may reference "eval_status"
613
+ // as an educational or historical reference, which is intentional.
614
+ let evalData;
615
+ try {
616
+ evalData = JSON.parse(text);
617
+ } catch (e) {
618
+ errors.push(`C5b [${rel}]: cannot parse JSON -- ${e.message}`);
619
+ continue;
620
+ }
621
+
622
+ function findDeprecatedKey(obj, jsonPath) {
623
+ if (Array.isArray(obj)) {
624
+ for (let idx = 0; idx < obj.length; idx++) {
625
+ findDeprecatedKey(obj[idx], `${jsonPath}[${idx}]`);
626
+ }
627
+ } else if (obj !== null && typeof obj === 'object') {
628
+ for (const key of Object.keys(obj)) {
629
+ if (key === 'eval_status') {
630
+ errors.push(
631
+ `C5b [${rel}]: eval artifact metadata uses deprecated v1 JSON key "eval_status" ` +
632
+ `at ${jsonPath}.${key}. ` +
633
+ `Post-SH-5784, use "eval_artifacts", "eval_state", and "routing_eval" instead.`
634
+ );
635
+ } else {
636
+ findDeprecatedKey(obj[key], `${jsonPath}.${key}`);
637
+ }
638
+ }
639
+ }
640
+ }
641
+ findDeprecatedKey(evalData, '$');
642
+ }
643
+
644
+ if (VERBOSE && errors.length === 0) {
645
+ console.log(
646
+ ` C5: OK -- ${scorecardFiles.length} scorecard(s), ${evalFiles.length} eval file(s) ` +
647
+ `pass all example truth invariants`
648
+ );
649
+ }
650
+
651
+ return errors;
652
+ }
653
+
654
+ // ---------------------------------------------------------------------------
655
+ // C6 -- Versioned schema parity (version-aware)
656
+ // ---------------------------------------------------------------------------
657
+
658
+ /**
659
+ * Resolve the current schema version from the unversioned skill schema.
660
+ * Looks at the `schema_version.const` variant in the oneOf or at the top-level
661
+ * const. Returns a number (e.g. 2, 3) or null if it cannot be determined.
662
+ */
663
+ function resolveCurrentSchemaVersion(skillSchema) {
664
+ const sv = skillSchema && skillSchema.properties && skillSchema.properties.schema_version;
665
+ if (!sv) return null;
666
+ if (Number.isInteger(sv.const)) return sv.const;
667
+ if (Array.isArray(sv.oneOf)) {
668
+ for (const variant of sv.oneOf) {
669
+ if (Number.isInteger(variant.const)) return variant.const;
670
+ if (typeof variant.const === 'string' && /^\d+$/.test(variant.const)) return parseInt(variant.const, 10);
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+
676
+ /**
677
+ * Check C6 — schema parity across the authored / generated / pinned files.
678
+ *
679
+ * The invariants:
680
+ * (a) The unversioned schemas (`skill.schema.json`, `manifest.schema.json`)
681
+ * must be content-identical to the pinned copy of the current version
682
+ * (`skill.v{N}.schema.json`, `manifest.v{N}.schema.json`), modulo
683
+ * `$id` and `title`.
684
+ * (b) All prior pinned versions (v2 when v3 is current, etc.) are treated
685
+ * as FROZEN: they must still exist and be readable, but they are NOT
686
+ * checked against the unversioned files. Freezing them is the whole
687
+ * point of pinning.
688
+ *
689
+ * Returns a list of error strings. Empty array means pass.
690
+ */
691
+ function checkC6VersionedSchemaParity() {
692
+ const errors = [];
693
+
694
+ const skillUnversioned = readJson(path.join(REPO_ROOT, 'schemas/skill.schema.json'));
695
+ const manifestUnversioned = readJson(path.join(REPO_ROOT, 'schemas/manifest.schema.json'));
696
+ if (!skillUnversioned) { errors.push('schemas/skill.schema.json: missing or unreadable'); return errors; }
697
+ if (!manifestUnversioned) { errors.push('schemas/manifest.schema.json: missing or unreadable'); return errors; }
698
+
699
+ const current = resolveCurrentSchemaVersion(skillUnversioned);
700
+ if (!current) {
701
+ errors.push('schemas/skill.schema.json: cannot determine current schema_version for parity check');
702
+ return errors;
703
+ }
704
+
705
+ // (a) Current-version pinned copies must match the unversioned files.
706
+ const currentPairs = [
707
+ { unversioned: 'schemas/skill.schema.json', versioned: `schemas/skill.v${current}.schema.json`, data: skillUnversioned },
708
+ { unversioned: 'schemas/manifest.schema.json', versioned: `schemas/manifest.v${current}.schema.json`, data: manifestUnversioned },
709
+ ];
710
+
711
+ const stripped = (obj) => {
712
+ const copy = JSON.parse(JSON.stringify(obj));
713
+ delete copy.$id;
714
+ delete copy.title;
715
+ return copy;
716
+ };
717
+
718
+ for (const { unversioned, versioned, data } of currentPairs) {
719
+ const v = readJson(path.join(REPO_ROOT, versioned));
720
+ if (!v) { errors.push(`${versioned}: missing — pinned copy of current schema version ${current} must exist`); continue; }
721
+ const uStr = JSON.stringify(stripped(data));
722
+ const vStr = JSON.stringify(stripped(v));
723
+ if (uStr !== vStr) {
724
+ errors.push(`${versioned} is out of sync with ${unversioned} (content differs modulo $id/title). After editing the unversioned schema, copy it to the versioned file and keep the v${current} $id/title.`);
725
+ }
726
+ if (VERBOSE && uStr === vStr) console.log(` ${versioned}: tracks ${unversioned}`);
727
+ }
728
+
729
+ // (b) Prior-version pinned copies must exist but are not parity-checked.
730
+ for (let v = 2; v < current; v++) {
731
+ for (const kind of ['skill', 'manifest']) {
732
+ const frozenPath = `schemas/${kind}.v${v}.schema.json`;
733
+ if (!fs.existsSync(path.join(REPO_ROOT, frozenPath))) {
734
+ errors.push(`${frozenPath}: missing — frozen prior-version schema must remain in the repo for consumers pinned to v${v}`);
735
+ } else if (VERBOSE) {
736
+ console.log(` ${frozenPath}: frozen (parity not checked)`);
737
+ }
738
+ }
739
+ }
740
+
741
+ return errors;
742
+ }
743
+
744
+ // ---------------------------------------------------------------------------
745
+ // C7 -- Generated field-reference parity
746
+ // ---------------------------------------------------------------------------
747
+
748
+ /**
749
+ * Check C7 — `docs/field-reference.generated.md` must match live regeneration
750
+ * from the current pinned schema description strings via
751
+ * `scripts/build-field-reference.js`.
752
+ *
753
+ * Invariant: the generated index is a deterministic projection of the schema's
754
+ * description fields. If the schema changes (a description is added, edited, or
755
+ * a new field is introduced) without regenerating the index, the docs drift
756
+ * silently. C7 closes this loop.
757
+ *
758
+ * Implementation strategy: spawn `node scripts/build-field-reference.js --check`
759
+ * which performs the regeneration in-memory and exits non-zero when the live
760
+ * file differs from regenerated output. C7 surfaces that exit signal as a
761
+ * structured protocol-consistency error.
762
+ *
763
+ * Returns a list of error strings. Empty array means pass.
764
+ */
765
+ function checkC7GeneratedFieldReferenceParity() {
766
+ const errors = [];
767
+ const builderPath = path.join(REPO_ROOT, 'scripts', 'build-field-reference.js');
768
+ const generatedPath = path.join(REPO_ROOT, 'docs', 'field-reference.generated.md');
769
+
770
+ if (!fs.existsSync(builderPath)) {
771
+ errors.push('scripts/build-field-reference.js: missing — required to regenerate docs/field-reference.generated.md');
772
+ return errors;
773
+ }
774
+ if (!fs.existsSync(generatedPath)) {
775
+ errors.push('docs/field-reference.generated.md: missing — run `node scripts/build-field-reference.js` to generate');
776
+ return errors;
777
+ }
778
+
779
+ const result = require('child_process').spawnSync(
780
+ process.execPath,
781
+ [builderPath, '--check'],
782
+ { cwd: REPO_ROOT, encoding: 'utf8' }
783
+ );
784
+
785
+ if (result.error) {
786
+ errors.push(`C7 [docs/field-reference.generated.md]: cannot invoke build-field-reference.js — ${result.error.message}`);
787
+ return errors;
788
+ }
789
+
790
+ if (result.status !== 0) {
791
+ errors.push(
792
+ `docs/field-reference.generated.md is out of step with the current skill schema description strings. ` +
793
+ `Run \`node scripts/build-field-reference.js\` to regenerate, then commit the result alongside any schema description edits.`
794
+ );
795
+ if (VERBOSE && result.stderr) {
796
+ console.error(result.stderr.trim());
797
+ }
798
+ }
799
+
800
+ return errors;
801
+ }
802
+
803
+ // ---------------------------------------------------------------------------
804
+ // C8 -- JSON-LD context coverage
805
+ // ---------------------------------------------------------------------------
806
+
807
+ /**
808
+ * Check C8 - `schemas/skill.context.jsonld` must cover every top-level
809
+ * authored field in `schemas/skill.schema.json`.
810
+ *
811
+ * ADR 0002 makes the JSON-LD context the FAIR interoperability bridge. If a
812
+ * new top-level schema field is added without a context term, RDF consumers
813
+ * silently lose that field. This check turns the prior hand-review rule into a
814
+ * deterministic cross-artifact gate.
815
+ *
816
+ * It also validates compact IRI prefixes used by context mappings. For example,
817
+ * `"keywords": "dcat:keyword"` requires a top-level `"dcat": "..."` namespace
818
+ * declaration in the same `@context` object.
819
+ */
820
+ function checkC8JsonLdContextCoverage() {
821
+ const errors = [];
822
+ const schemaPath = path.join(REPO_ROOT, 'schemas', 'skill.schema.json');
823
+ const contextPath = path.join(REPO_ROOT, 'schemas', 'skill.context.jsonld');
824
+
825
+ const schema = readJson(schemaPath);
826
+ if (!schema || !schema.properties) {
827
+ errors.push('C8 [schemas/skill.schema.json]: cannot read schema -- JSON-LD context coverage check skipped');
828
+ return errors;
829
+ }
830
+
831
+ const contextDoc = readJson(contextPath);
832
+ const context = contextDoc && contextDoc['@context'];
833
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
834
+ errors.push('C8 [schemas/skill.context.jsonld]: missing or invalid @context object');
835
+ return errors;
836
+ }
837
+
838
+ const schemaFields = Object.keys(schema.properties);
839
+ const contextKeys = new Set(Object.keys(context));
840
+ const missing = schemaFields.filter(field => !contextKeys.has(field));
841
+
842
+ if (missing.length > 0) {
843
+ errors.push(
844
+ `C8 [schemas/skill.context.jsonld]: ${missing.length} top-level schema field(s) missing from @context: ` +
845
+ missing.map(f => `"${f}"`).join(', ')
846
+ );
847
+ }
848
+
849
+ const declaredPrefixes = new Set();
850
+ for (const [key, value] of Object.entries(context)) {
851
+ if (typeof value === 'string' && /^https?:\/\//.test(value)) {
852
+ declaredPrefixes.add(key);
853
+ }
854
+ }
855
+
856
+ const prefixUses = [];
857
+ function collectCompactIris(value, jsonPath) {
858
+ if (typeof value === 'string') {
859
+ const m = value.match(/^([A-Za-z][A-Za-z0-9_-]*):[^/]/);
860
+ if (m) prefixUses.push({ prefix: m[1], value, jsonPath });
861
+ return;
862
+ }
863
+ if (Array.isArray(value)) {
864
+ value.forEach((item, idx) => collectCompactIris(item, `${jsonPath}[${idx}]`));
865
+ return;
866
+ }
867
+ if (value && typeof value === 'object') {
868
+ for (const [key, subValue] of Object.entries(value)) {
869
+ if (key.startsWith('_')) continue;
870
+ collectCompactIris(subValue, `${jsonPath}.${key}`);
871
+ }
872
+ }
873
+ }
874
+
875
+ for (const [key, value] of Object.entries(context)) {
876
+ if (declaredPrefixes.has(key)) continue;
877
+ collectCompactIris(value, `@context.${key}`);
878
+ }
879
+
880
+ for (const use of prefixUses) {
881
+ if (!declaredPrefixes.has(use.prefix)) {
882
+ errors.push(
883
+ `C8 [schemas/skill.context.jsonld]: ${use.jsonPath} uses compact IRI ` +
884
+ `"${use.value}" but prefix "${use.prefix}" is not declared in @context`
885
+ );
886
+ }
887
+ }
888
+
889
+ if (VERBOSE && errors.length === 0) {
890
+ console.log(
891
+ ` C8: OK -- ${schemaFields.length} top-level schema fields covered; ` +
892
+ `${declaredPrefixes.size} namespace prefix(es) declared`
893
+ );
894
+ }
895
+
896
+ return errors;
897
+ }
898
+
899
+ // ---------------------------------------------------------------------------
900
+ // Main
901
+ // ---------------------------------------------------------------------------
902
+
903
+ function main() {
904
+ console.log('Running protocol consistency checks...\n');
905
+
906
+ const allErrors = [];
907
+ const allWarnings = [];
908
+
909
+ // C1
910
+ process.stdout.write('C1 Field-set parity... ');
911
+ const c1 = checkC1FieldSetParity();
912
+ if (c1.length === 0) console.log('OK');
913
+ else { console.log('FAIL'); for (const e of c1) { console.error(` ERROR ${e}`); } }
914
+ allErrors.push(...c1);
915
+
916
+ // C2
917
+ process.stdout.write('C2 Authored-to-generated parity... ');
918
+ const c2 = checkC2AuthoredToGeneratedParity();
919
+ if (c2.length === 0) console.log('OK');
920
+ else { console.log('FAIL'); for (const e of c2) { console.error(` ERROR ${e}`); } }
921
+ allErrors.push(...c2);
922
+
923
+ // C3 (warnings -- does not affect exit code)
924
+ process.stdout.write('C3 Artifact-root convention... ');
925
+ const c3 = checkC3ArtifactRootConvention();
926
+ if (c3.length === 0) console.log('OK');
927
+ else {
928
+ console.log(`WARN (${c3.length})`);
929
+ for (const w of c3) { console.warn(` WARN ${w}`); }
930
+ }
931
+ allWarnings.push(...c3);
932
+
933
+ // C4
934
+ process.stdout.write('C4 Sample manifest correctness... ');
935
+ const c4 = checkC4SampleManifestCorrectness();
936
+ if (c4.length === 0) console.log('OK');
937
+ else { console.log('FAIL'); for (const e of c4) { console.error(` ERROR ${e}`); } }
938
+ allErrors.push(...c4);
939
+
940
+ // C5
941
+ process.stdout.write('C5 Example truth invariants... ');
942
+ const c5 = checkC5ExampleTruthInvariants();
943
+ if (c5.length === 0) console.log('OK');
944
+ else { console.log('FAIL'); for (const e of c5) { console.error(` ERROR ${e}`); } }
945
+ allErrors.push(...c5);
946
+
947
+ // C6
948
+ process.stdout.write('C6 Versioned schema parity... ');
949
+ const c6 = checkC6VersionedSchemaParity();
950
+ if (c6.length === 0) console.log('OK');
951
+ else { console.log('FAIL'); for (const e of c6) { console.error(` ERROR ${e}`); } }
952
+ allErrors.push(...c6);
953
+
954
+ // C7
955
+ process.stdout.write('C7 Generated field-reference parity... ');
956
+ const c7 = checkC7GeneratedFieldReferenceParity();
957
+ if (c7.length === 0) console.log('OK');
958
+ else { console.log('FAIL'); for (const e of c7) { console.error(` ERROR ${e}`); } }
959
+ allErrors.push(...c7);
960
+
961
+ // C8
962
+ process.stdout.write('C8 JSON-LD context coverage... ');
963
+ const c8 = checkC8JsonLdContextCoverage();
964
+ if (c8.length === 0) console.log('OK');
965
+ else { console.log('FAIL'); for (const e of c8) { console.error(` ERROR ${e}`); } }
966
+ allErrors.push(...c8);
967
+
968
+ console.log('');
969
+
970
+ if (allErrors.length > 0) {
971
+ console.error(`FAIL: ${allErrors.length} error(s) found. ${allWarnings.length} warning(s).`);
972
+ process.exit(1);
973
+ } else {
974
+ console.log(`PASS: all protocol consistency checks passed. ${allWarnings.length} warning(s).`);
975
+ process.exit(0);
976
+ }
977
+ }
978
+
979
+ main();