@nebulacomponents/citable 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +29 -2
  3. package/dist/universal/.agents/skills/citable/SKILL.md +14 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +2 -1
  6. package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +28 -0
  7. package/dist/universal/.agents/skills/citable/commands/field-validation.md +71 -0
  8. package/dist/universal/.agents/skills/citable/commands/observe-and-act.md +13 -1
  9. package/dist/universal/.agents/skills/citable/manifest.json +30 -10
  10. package/dist/universal/.agents/skills/citable/references/aeo-geo-validation.md +1 -1
  11. package/dist/universal/.agents/skills/citable/references/capability-boundaries.md +3 -0
  12. package/dist/universal/.agents/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  13. package/dist/universal/.agents/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  14. package/dist/universal/.agents/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  15. package/dist/universal/.agents/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  16. package/dist/universal/.agents/skills/citable/schemas/assumption.schema.json +119 -0
  17. package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  18. package/dist/universal/.agents/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  19. package/dist/universal/.agents/skills/citable/schemas/customer-outcome.schema.json +119 -0
  20. package/dist/universal/.agents/skills/citable/schemas/decision.schema.json +154 -0
  21. package/dist/universal/.agents/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  22. package/dist/universal/.agents/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  23. package/dist/universal/.agents/skills/citable/schemas/initiative.schema.json +185 -0
  24. package/dist/universal/.agents/skills/citable/schemas/kpi.schema.json +108 -0
  25. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  26. package/dist/universal/.agents/skills/citable/schemas/release-manifest.schema.json +74 -0
  27. package/dist/universal/.agents/skills/citable/schemas/release-state.schema.json +40 -0
  28. package/dist/universal/.agents/skills/citable/schemas/risk.schema.json +172 -0
  29. package/dist/universal/.agents/skills/citable/schemas/scenario.schema.json +165 -0
  30. package/dist/universal/.agents/skills/citable/schemas/variance.schema.json +112 -0
  31. package/dist/universal/.claude/skills/citable/SKILL.md +14 -1
  32. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  33. package/dist/universal/.claude/skills/citable/commands/README.md +2 -1
  34. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +28 -0
  35. package/dist/universal/.claude/skills/citable/commands/field-validation.md +71 -0
  36. package/dist/universal/.claude/skills/citable/commands/observe-and-act.md +13 -1
  37. package/dist/universal/.claude/skills/citable/manifest.json +30 -10
  38. package/dist/universal/.claude/skills/citable/references/aeo-geo-validation.md +1 -1
  39. package/dist/universal/.claude/skills/citable/references/capability-boundaries.md +3 -0
  40. package/dist/universal/.claude/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  41. package/dist/universal/.claude/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  42. package/dist/universal/.claude/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  43. package/dist/universal/.claude/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  44. package/dist/universal/.claude/skills/citable/schemas/assumption.schema.json +119 -0
  45. package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  46. package/dist/universal/.claude/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  47. package/dist/universal/.claude/skills/citable/schemas/customer-outcome.schema.json +119 -0
  48. package/dist/universal/.claude/skills/citable/schemas/decision.schema.json +154 -0
  49. package/dist/universal/.claude/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  50. package/dist/universal/.claude/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  51. package/dist/universal/.claude/skills/citable/schemas/initiative.schema.json +185 -0
  52. package/dist/universal/.claude/skills/citable/schemas/kpi.schema.json +108 -0
  53. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  54. package/dist/universal/.claude/skills/citable/schemas/release-manifest.schema.json +74 -0
  55. package/dist/universal/.claude/skills/citable/schemas/release-state.schema.json +40 -0
  56. package/dist/universal/.claude/skills/citable/schemas/risk.schema.json +172 -0
  57. package/dist/universal/.claude/skills/citable/schemas/scenario.schema.json +165 -0
  58. package/dist/universal/.claude/skills/citable/schemas/variance.schema.json +112 -0
  59. package/dist/universal/.cursor/skills/citable/SKILL.md +14 -1
  60. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  61. package/dist/universal/.cursor/skills/citable/commands/README.md +2 -1
  62. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +28 -0
  63. package/dist/universal/.cursor/skills/citable/commands/field-validation.md +71 -0
  64. package/dist/universal/.cursor/skills/citable/commands/observe-and-act.md +13 -1
  65. package/dist/universal/.cursor/skills/citable/manifest.json +30 -10
  66. package/dist/universal/.cursor/skills/citable/references/aeo-geo-validation.md +1 -1
  67. package/dist/universal/.cursor/skills/citable/references/capability-boundaries.md +3 -0
  68. package/dist/universal/.cursor/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  69. package/dist/universal/.cursor/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  70. package/dist/universal/.cursor/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  71. package/dist/universal/.cursor/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  72. package/dist/universal/.cursor/skills/citable/schemas/assumption.schema.json +119 -0
  73. package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  74. package/dist/universal/.cursor/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  75. package/dist/universal/.cursor/skills/citable/schemas/customer-outcome.schema.json +119 -0
  76. package/dist/universal/.cursor/skills/citable/schemas/decision.schema.json +154 -0
  77. package/dist/universal/.cursor/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  78. package/dist/universal/.cursor/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  79. package/dist/universal/.cursor/skills/citable/schemas/initiative.schema.json +185 -0
  80. package/dist/universal/.cursor/skills/citable/schemas/kpi.schema.json +108 -0
  81. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  82. package/dist/universal/.cursor/skills/citable/schemas/release-manifest.schema.json +74 -0
  83. package/dist/universal/.cursor/skills/citable/schemas/release-state.schema.json +40 -0
  84. package/dist/universal/.cursor/skills/citable/schemas/risk.schema.json +172 -0
  85. package/dist/universal/.cursor/skills/citable/schemas/scenario.schema.json +165 -0
  86. package/dist/universal/.cursor/skills/citable/schemas/variance.schema.json +112 -0
  87. package/dist/universal/.gemini/skills/citable/SKILL.md +14 -1
  88. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  89. package/dist/universal/.gemini/skills/citable/commands/README.md +2 -1
  90. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +28 -0
  91. package/dist/universal/.gemini/skills/citable/commands/field-validation.md +71 -0
  92. package/dist/universal/.gemini/skills/citable/commands/observe-and-act.md +13 -1
  93. package/dist/universal/.gemini/skills/citable/manifest.json +30 -10
  94. package/dist/universal/.gemini/skills/citable/references/aeo-geo-validation.md +1 -1
  95. package/dist/universal/.gemini/skills/citable/references/capability-boundaries.md +3 -0
  96. package/dist/universal/.gemini/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  97. package/dist/universal/.gemini/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  98. package/dist/universal/.gemini/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  99. package/dist/universal/.gemini/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  100. package/dist/universal/.gemini/skills/citable/schemas/assumption.schema.json +119 -0
  101. package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  102. package/dist/universal/.gemini/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  103. package/dist/universal/.gemini/skills/citable/schemas/customer-outcome.schema.json +119 -0
  104. package/dist/universal/.gemini/skills/citable/schemas/decision.schema.json +154 -0
  105. package/dist/universal/.gemini/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  106. package/dist/universal/.gemini/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  107. package/dist/universal/.gemini/skills/citable/schemas/initiative.schema.json +185 -0
  108. package/dist/universal/.gemini/skills/citable/schemas/kpi.schema.json +108 -0
  109. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  110. package/dist/universal/.gemini/skills/citable/schemas/release-manifest.schema.json +74 -0
  111. package/dist/universal/.gemini/skills/citable/schemas/release-state.schema.json +40 -0
  112. package/dist/universal/.gemini/skills/citable/schemas/risk.schema.json +172 -0
  113. package/dist/universal/.gemini/skills/citable/schemas/scenario.schema.json +165 -0
  114. package/dist/universal/.gemini/skills/citable/schemas/variance.schema.json +112 -0
  115. package/dist/universal/.github/skills/citable/SKILL.md +14 -1
  116. package/dist/universal/.github/skills/citable/VERSION +1 -1
  117. package/dist/universal/.github/skills/citable/commands/README.md +2 -1
  118. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +28 -0
  119. package/dist/universal/.github/skills/citable/commands/field-validation.md +71 -0
  120. package/dist/universal/.github/skills/citable/commands/observe-and-act.md +13 -1
  121. package/dist/universal/.github/skills/citable/manifest.json +30 -10
  122. package/dist/universal/.github/skills/citable/references/aeo-geo-validation.md +1 -1
  123. package/dist/universal/.github/skills/citable/references/capability-boundaries.md +3 -0
  124. package/dist/universal/.github/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  125. package/dist/universal/.github/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  126. package/dist/universal/.github/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  127. package/dist/universal/.github/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  128. package/dist/universal/.github/skills/citable/schemas/assumption.schema.json +119 -0
  129. package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  130. package/dist/universal/.github/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  131. package/dist/universal/.github/skills/citable/schemas/customer-outcome.schema.json +119 -0
  132. package/dist/universal/.github/skills/citable/schemas/decision.schema.json +154 -0
  133. package/dist/universal/.github/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  134. package/dist/universal/.github/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  135. package/dist/universal/.github/skills/citable/schemas/initiative.schema.json +185 -0
  136. package/dist/universal/.github/skills/citable/schemas/kpi.schema.json +108 -0
  137. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  138. package/dist/universal/.github/skills/citable/schemas/release-manifest.schema.json +74 -0
  139. package/dist/universal/.github/skills/citable/schemas/release-state.schema.json +40 -0
  140. package/dist/universal/.github/skills/citable/schemas/risk.schema.json +172 -0
  141. package/dist/universal/.github/skills/citable/schemas/scenario.schema.json +165 -0
  142. package/dist/universal/.github/skills/citable/schemas/variance.schema.json +112 -0
  143. package/dist/universal/.kiro/skills/citable/SKILL.md +14 -1
  144. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  145. package/dist/universal/.kiro/skills/citable/commands/README.md +2 -1
  146. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +28 -0
  147. package/dist/universal/.kiro/skills/citable/commands/field-validation.md +71 -0
  148. package/dist/universal/.kiro/skills/citable/commands/observe-and-act.md +13 -1
  149. package/dist/universal/.kiro/skills/citable/manifest.json +30 -10
  150. package/dist/universal/.kiro/skills/citable/references/aeo-geo-validation.md +1 -1
  151. package/dist/universal/.kiro/skills/citable/references/capability-boundaries.md +3 -0
  152. package/dist/universal/.kiro/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  153. package/dist/universal/.kiro/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  154. package/dist/universal/.kiro/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  155. package/dist/universal/.kiro/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  156. package/dist/universal/.kiro/skills/citable/schemas/assumption.schema.json +119 -0
  157. package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  158. package/dist/universal/.kiro/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  159. package/dist/universal/.kiro/skills/citable/schemas/customer-outcome.schema.json +119 -0
  160. package/dist/universal/.kiro/skills/citable/schemas/decision.schema.json +154 -0
  161. package/dist/universal/.kiro/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  162. package/dist/universal/.kiro/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  163. package/dist/universal/.kiro/skills/citable/schemas/initiative.schema.json +185 -0
  164. package/dist/universal/.kiro/skills/citable/schemas/kpi.schema.json +108 -0
  165. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  166. package/dist/universal/.kiro/skills/citable/schemas/release-manifest.schema.json +74 -0
  167. package/dist/universal/.kiro/skills/citable/schemas/release-state.schema.json +40 -0
  168. package/dist/universal/.kiro/skills/citable/schemas/risk.schema.json +172 -0
  169. package/dist/universal/.kiro/skills/citable/schemas/scenario.schema.json +165 -0
  170. package/dist/universal/.kiro/skills/citable/schemas/variance.schema.json +112 -0
  171. package/dist/universal/.opencode/skills/citable/SKILL.md +14 -1
  172. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  173. package/dist/universal/.opencode/skills/citable/commands/README.md +2 -1
  174. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +28 -0
  175. package/dist/universal/.opencode/skills/citable/commands/field-validation.md +71 -0
  176. package/dist/universal/.opencode/skills/citable/commands/observe-and-act.md +13 -1
  177. package/dist/universal/.opencode/skills/citable/manifest.json +30 -10
  178. package/dist/universal/.opencode/skills/citable/references/aeo-geo-validation.md +1 -1
  179. package/dist/universal/.opencode/skills/citable/references/capability-boundaries.md +3 -0
  180. package/dist/universal/.opencode/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  181. package/dist/universal/.opencode/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  182. package/dist/universal/.opencode/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  183. package/dist/universal/.opencode/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  184. package/dist/universal/.opencode/skills/citable/schemas/assumption.schema.json +119 -0
  185. package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  186. package/dist/universal/.opencode/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  187. package/dist/universal/.opencode/skills/citable/schemas/customer-outcome.schema.json +119 -0
  188. package/dist/universal/.opencode/skills/citable/schemas/decision.schema.json +154 -0
  189. package/dist/universal/.opencode/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  190. package/dist/universal/.opencode/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  191. package/dist/universal/.opencode/skills/citable/schemas/initiative.schema.json +185 -0
  192. package/dist/universal/.opencode/skills/citable/schemas/kpi.schema.json +108 -0
  193. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  194. package/dist/universal/.opencode/skills/citable/schemas/release-manifest.schema.json +74 -0
  195. package/dist/universal/.opencode/skills/citable/schemas/release-state.schema.json +40 -0
  196. package/dist/universal/.opencode/skills/citable/schemas/risk.schema.json +172 -0
  197. package/dist/universal/.opencode/skills/citable/schemas/scenario.schema.json +165 -0
  198. package/dist/universal/.opencode/skills/citable/schemas/variance.schema.json +112 -0
  199. package/dist/universal/.pi/agent/skills/citable/SKILL.md +14 -1
  200. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  201. package/dist/universal/.pi/agent/skills/citable/commands/README.md +2 -1
  202. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +28 -0
  203. package/dist/universal/.pi/agent/skills/citable/commands/field-validation.md +71 -0
  204. package/dist/universal/.pi/agent/skills/citable/commands/observe-and-act.md +13 -1
  205. package/dist/universal/.pi/agent/skills/citable/manifest.json +30 -10
  206. package/dist/universal/.pi/agent/skills/citable/references/aeo-geo-validation.md +1 -1
  207. package/dist/universal/.pi/agent/skills/citable/references/capability-boundaries.md +3 -0
  208. package/dist/universal/.pi/agent/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  209. package/dist/universal/.pi/agent/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  210. package/dist/universal/.pi/agent/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  211. package/dist/universal/.pi/agent/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  212. package/dist/universal/.pi/agent/skills/citable/schemas/assumption.schema.json +119 -0
  213. package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  214. package/dist/universal/.pi/agent/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  215. package/dist/universal/.pi/agent/skills/citable/schemas/customer-outcome.schema.json +119 -0
  216. package/dist/universal/.pi/agent/skills/citable/schemas/decision.schema.json +154 -0
  217. package/dist/universal/.pi/agent/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  218. package/dist/universal/.pi/agent/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  219. package/dist/universal/.pi/agent/skills/citable/schemas/initiative.schema.json +185 -0
  220. package/dist/universal/.pi/agent/skills/citable/schemas/kpi.schema.json +108 -0
  221. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  222. package/dist/universal/.pi/agent/skills/citable/schemas/release-manifest.schema.json +74 -0
  223. package/dist/universal/.pi/agent/skills/citable/schemas/release-state.schema.json +40 -0
  224. package/dist/universal/.pi/agent/skills/citable/schemas/risk.schema.json +172 -0
  225. package/dist/universal/.pi/agent/skills/citable/schemas/scenario.schema.json +165 -0
  226. package/dist/universal/.pi/agent/skills/citable/schemas/variance.schema.json +112 -0
  227. package/dist/universal/.qoder/skills/citable/SKILL.md +14 -1
  228. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  229. package/dist/universal/.qoder/skills/citable/commands/README.md +2 -1
  230. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +28 -0
  231. package/dist/universal/.qoder/skills/citable/commands/field-validation.md +71 -0
  232. package/dist/universal/.qoder/skills/citable/commands/observe-and-act.md +13 -1
  233. package/dist/universal/.qoder/skills/citable/manifest.json +30 -10
  234. package/dist/universal/.qoder/skills/citable/references/aeo-geo-validation.md +1 -1
  235. package/dist/universal/.qoder/skills/citable/references/capability-boundaries.md +3 -0
  236. package/dist/universal/.qoder/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  237. package/dist/universal/.qoder/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  238. package/dist/universal/.qoder/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  239. package/dist/universal/.qoder/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  240. package/dist/universal/.qoder/skills/citable/schemas/assumption.schema.json +119 -0
  241. package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  242. package/dist/universal/.qoder/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  243. package/dist/universal/.qoder/skills/citable/schemas/customer-outcome.schema.json +119 -0
  244. package/dist/universal/.qoder/skills/citable/schemas/decision.schema.json +154 -0
  245. package/dist/universal/.qoder/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  246. package/dist/universal/.qoder/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  247. package/dist/universal/.qoder/skills/citable/schemas/initiative.schema.json +185 -0
  248. package/dist/universal/.qoder/skills/citable/schemas/kpi.schema.json +108 -0
  249. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  250. package/dist/universal/.qoder/skills/citable/schemas/release-manifest.schema.json +74 -0
  251. package/dist/universal/.qoder/skills/citable/schemas/release-state.schema.json +40 -0
  252. package/dist/universal/.qoder/skills/citable/schemas/risk.schema.json +172 -0
  253. package/dist/universal/.qoder/skills/citable/schemas/scenario.schema.json +165 -0
  254. package/dist/universal/.qoder/skills/citable/schemas/variance.schema.json +112 -0
  255. package/dist/universal/.rovodev/skills/citable/SKILL.md +14 -1
  256. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  257. package/dist/universal/.rovodev/skills/citable/commands/README.md +2 -1
  258. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +28 -0
  259. package/dist/universal/.rovodev/skills/citable/commands/field-validation.md +71 -0
  260. package/dist/universal/.rovodev/skills/citable/commands/observe-and-act.md +13 -1
  261. package/dist/universal/.rovodev/skills/citable/manifest.json +30 -10
  262. package/dist/universal/.rovodev/skills/citable/references/aeo-geo-validation.md +1 -1
  263. package/dist/universal/.rovodev/skills/citable/references/capability-boundaries.md +3 -0
  264. package/dist/universal/.rovodev/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  265. package/dist/universal/.rovodev/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  266. package/dist/universal/.rovodev/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  267. package/dist/universal/.rovodev/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  268. package/dist/universal/.rovodev/skills/citable/schemas/assumption.schema.json +119 -0
  269. package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  270. package/dist/universal/.rovodev/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  271. package/dist/universal/.rovodev/skills/citable/schemas/customer-outcome.schema.json +119 -0
  272. package/dist/universal/.rovodev/skills/citable/schemas/decision.schema.json +154 -0
  273. package/dist/universal/.rovodev/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  274. package/dist/universal/.rovodev/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  275. package/dist/universal/.rovodev/skills/citable/schemas/initiative.schema.json +185 -0
  276. package/dist/universal/.rovodev/skills/citable/schemas/kpi.schema.json +108 -0
  277. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  278. package/dist/universal/.rovodev/skills/citable/schemas/release-manifest.schema.json +74 -0
  279. package/dist/universal/.rovodev/skills/citable/schemas/release-state.schema.json +40 -0
  280. package/dist/universal/.rovodev/skills/citable/schemas/risk.schema.json +172 -0
  281. package/dist/universal/.rovodev/skills/citable/schemas/scenario.schema.json +165 -0
  282. package/dist/universal/.rovodev/skills/citable/schemas/variance.schema.json +112 -0
  283. package/dist/universal/.trae/skills/citable/SKILL.md +14 -1
  284. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  285. package/dist/universal/.trae/skills/citable/commands/README.md +2 -1
  286. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +28 -0
  287. package/dist/universal/.trae/skills/citable/commands/field-validation.md +71 -0
  288. package/dist/universal/.trae/skills/citable/commands/observe-and-act.md +13 -1
  289. package/dist/universal/.trae/skills/citable/manifest.json +30 -10
  290. package/dist/universal/.trae/skills/citable/references/aeo-geo-validation.md +1 -1
  291. package/dist/universal/.trae/skills/citable/references/capability-boundaries.md +3 -0
  292. package/dist/universal/.trae/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  293. package/dist/universal/.trae/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  294. package/dist/universal/.trae/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  295. package/dist/universal/.trae/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  296. package/dist/universal/.trae/skills/citable/schemas/assumption.schema.json +119 -0
  297. package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  298. package/dist/universal/.trae/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  299. package/dist/universal/.trae/skills/citable/schemas/customer-outcome.schema.json +119 -0
  300. package/dist/universal/.trae/skills/citable/schemas/decision.schema.json +154 -0
  301. package/dist/universal/.trae/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  302. package/dist/universal/.trae/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  303. package/dist/universal/.trae/skills/citable/schemas/initiative.schema.json +185 -0
  304. package/dist/universal/.trae/skills/citable/schemas/kpi.schema.json +108 -0
  305. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  306. package/dist/universal/.trae/skills/citable/schemas/release-manifest.schema.json +74 -0
  307. package/dist/universal/.trae/skills/citable/schemas/release-state.schema.json +40 -0
  308. package/dist/universal/.trae/skills/citable/schemas/risk.schema.json +172 -0
  309. package/dist/universal/.trae/skills/citable/schemas/scenario.schema.json +165 -0
  310. package/dist/universal/.trae/skills/citable/schemas/variance.schema.json +112 -0
  311. package/dist/universal/.trae-cn/skills/citable/SKILL.md +14 -1
  312. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  313. package/dist/universal/.trae-cn/skills/citable/commands/README.md +2 -1
  314. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +28 -0
  315. package/dist/universal/.trae-cn/skills/citable/commands/field-validation.md +71 -0
  316. package/dist/universal/.trae-cn/skills/citable/commands/observe-and-act.md +13 -1
  317. package/dist/universal/.trae-cn/skills/citable/manifest.json +30 -10
  318. package/dist/universal/.trae-cn/skills/citable/references/aeo-geo-validation.md +1 -1
  319. package/dist/universal/.trae-cn/skills/citable/references/capability-boundaries.md +3 -0
  320. package/dist/universal/.trae-cn/skills/citable/schemas/acceptance-corpus.schema.json +168 -0
  321. package/dist/universal/.trae-cn/skills/citable/schemas/acceptance-run-context.schema.json +17 -0
  322. package/dist/universal/.trae-cn/skills/citable/schemas/acceptance-run-receipt.schema.json +88 -0
  323. package/dist/universal/.trae-cn/skills/citable/schemas/artifact-interchange.schema.json +58 -0
  324. package/dist/universal/.trae-cn/skills/citable/schemas/assumption.schema.json +119 -0
  325. package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +76 -0
  326. package/dist/universal/.trae-cn/skills/citable/schemas/corpus-publication-receipt.schema.json +24 -0
  327. package/dist/universal/.trae-cn/skills/citable/schemas/customer-outcome.schema.json +119 -0
  328. package/dist/universal/.trae-cn/skills/citable/schemas/decision.schema.json +154 -0
  329. package/dist/universal/.trae-cn/skills/citable/schemas/deployment-receipt.schema.json +51 -0
  330. package/dist/universal/.trae-cn/skills/citable/schemas/field-validation-metrics.schema.json +15 -0
  331. package/dist/universal/.trae-cn/skills/citable/schemas/initiative.schema.json +185 -0
  332. package/dist/universal/.trae-cn/skills/citable/schemas/kpi.schema.json +108 -0
  333. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  334. package/dist/universal/.trae-cn/skills/citable/schemas/release-manifest.schema.json +74 -0
  335. package/dist/universal/.trae-cn/skills/citable/schemas/release-state.schema.json +40 -0
  336. package/dist/universal/.trae-cn/skills/citable/schemas/risk.schema.json +172 -0
  337. package/dist/universal/.trae-cn/skills/citable/schemas/scenario.schema.json +165 -0
  338. package/dist/universal/.trae-cn/skills/citable/schemas/variance.schema.json +112 -0
  339. package/dist/universal/manifest.json +363 -123
  340. package/package.json +12 -1
  341. package/release/surfaces.json +21 -0
  342. package/schemas/acceptance-corpus.schema.json +168 -0
  343. package/schemas/acceptance-run-context.schema.json +17 -0
  344. package/schemas/acceptance-run-receipt.schema.json +88 -0
  345. package/schemas/artifact-interchange.schema.json +58 -0
  346. package/schemas/assumption.schema.json +119 -0
  347. package/schemas/browser-evidence-plan.schema.json +76 -0
  348. package/schemas/corpus-publication-receipt.schema.json +24 -0
  349. package/schemas/customer-outcome.schema.json +119 -0
  350. package/schemas/decision.schema.json +154 -0
  351. package/schemas/deployment-receipt.schema.json +51 -0
  352. package/schemas/field-validation-metrics.schema.json +15 -0
  353. package/schemas/initiative.schema.json +185 -0
  354. package/schemas/kpi.schema.json +108 -0
  355. package/schemas/observation.schema.json +1 -1
  356. package/schemas/release-manifest.schema.json +74 -0
  357. package/schemas/release-state.schema.json +40 -0
  358. package/schemas/risk.schema.json +172 -0
  359. package/schemas/scenario.schema.json +165 -0
  360. package/schemas/variance.schema.json +112 -0
  361. package/scripts/release-governance.js +123 -0
  362. package/skill/SKILL.md +14 -1
  363. package/skill/commands/README.md +2 -1
  364. package/skill/commands/artifact-interchange.md +28 -0
  365. package/skill/commands/field-validation.md +71 -0
  366. package/skill/commands/observe-and-act.md +13 -1
  367. package/skill/references/aeo-geo-validation.md +1 -1
  368. package/skill/references/capability-boundaries.md +3 -0
  369. package/src/acceptance/reproducibility.js +180 -0
  370. package/src/artifacts/interchange.js +179 -0
  371. package/src/cli/index.js +149 -4
  372. package/src/commands/assumptionAudit.js +111 -0
  373. package/src/commands/audit.js +6 -0
  374. package/src/commands/boardReport.js +251 -0
  375. package/src/commands/competitiveIntel.js +124 -0
  376. package/src/commands/corpus.js +322 -0
  377. package/src/commands/decisionMemo.js +202 -0
  378. package/src/commands/executive.js +167 -0
  379. package/src/commands/executiveReview.js +206 -0
  380. package/src/commands/init.js +14 -1
  381. package/src/commands/kpi.js +86 -0
  382. package/src/commands/monitor.js +9 -0
  383. package/src/commands/observe.js +202 -30
  384. package/src/commands/outcomes.js +113 -0
  385. package/src/commands/prioritize.js +108 -0
  386. package/src/commands/risk.js +104 -0
  387. package/src/commands/scenario.js +98 -0
  388. package/src/commands/variance.js +102 -0
  389. package/src/detectors/framework.js +37 -0
  390. package/src/detectors/page.js +10 -10
  391. package/src/detectors/tech.js +5 -3
  392. package/src/extractor/site.js +21 -6
  393. package/src/observations/browserJourney.js +167 -0
  394. package/src/observations/media.js +8 -1
  395. package/src/registries/index.js +107 -6
  396. package/src/release/governance.js +331 -0
  397. package/src/shared/asOf.js +18 -0
@@ -8,11 +8,20 @@ import { parse as parseCsv } from 'csv-parse/sync';
8
8
  import { crawlerIdentity } from '../observations/crawlerIdentity.js';
9
9
  import { validateAgainst } from '../shared/schemaValidator.js';
10
10
  import { observeMedia } from '../observations/media.js';
11
+ import { parse as parseHtml } from 'node-html-parser';
12
+ import { verifyManifestIntegrity } from '../release/governance.js';
13
+ import { observeBrowserPlan } from '../observations/browserJourney.js';
11
14
 
12
15
  const originOf = (value) => { try { return new URL(value).origin; } catch { return null; } };
13
16
  const words = (text) => String(text || '').trim().split(/\s+/).filter(Boolean);
14
17
  const strictDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value || '') && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
15
18
 
19
+ function visibleHtmlText(html) {
20
+ const document = parseHtml(String(html || ''));
21
+ for (const node of document.querySelectorAll('script,style')) node.remove();
22
+ return document.textContent.replace(/\s+/g, ' ').trim();
23
+ }
24
+
16
25
  function canonicalReview(raw, targetOrigin) {
17
26
  const citations = raw.citations || [];
18
27
  return citations.map((citation, index) => {
@@ -31,38 +40,61 @@ function canonicalReview(raw, targetOrigin) {
31
40
  }
32
41
 
33
42
  async function observeRender(root, options) {
43
+ if (options.input) return observeBrowserPlan(root, options);
34
44
  if (!options.target || !/^https?:\/\//.test(options.target)) throw new Error('render requires --target <http(s) URL>');
35
- let playwright;
36
- try { playwright = await import('playwright'); } catch {
37
- const item = envelope('render', { url: options.target }, { method: 'browser', source: 'playwright', state: 'not_evidenced', confidence: 'unknown', limitations: ['Optional Playwright dependency is not installed.'] });
38
- return observationRun(root, 'observe render', options.target, [item], { incomplete: ['Rendered DOM capture unavailable: install Playwright and a Chromium browser.'] });
45
+ const profileNames = ['desktop', 'mobile', 'javascript_disabled'];
46
+ let reused = [], previousRaw = null;
47
+ if (options.resumeRun) {
48
+ const previousDir = path.join(root, '.citable', 'runs', options.resumeRun);
49
+ const observationsDir = path.join(previousDir, 'observations');
50
+ if (!fs.existsSync(observationsDir)) throw new Error(`resume run ${options.resumeRun} has no observations`);
51
+ reused = fs.readdirSync(observationsDir).filter((name) => name.endsWith('-render.json')).map((name) => JSON.parse(fs.readFileSync(path.join(observationsDir, name)))).filter((item) => item.state === 'observed' && profileNames.includes(item.data.profile) && item.data.url === options.target && Boolean(item.data.interaction_execution_requested) === Boolean(options.interactions));
52
+ previousRaw = fs.readFileSync(path.join(previousDir, 'manifest.json'), 'utf8');
53
+ }
54
+ let browser = null;
55
+ let capture = options.captureProfile;
56
+ if (!capture) {
57
+ let playwright;
58
+ try { playwright = await import('playwright'); } catch {
59
+ const item = envelope('render', { url: options.target }, { method: 'browser', source: 'playwright', state: 'not_evidenced', confidence: 'unknown', limitations: ['Optional Playwright dependency is not installed.'] });
60
+ return observationRun(root, 'observe render', options.target, [item], { incomplete: ['Rendered DOM capture unavailable: install Playwright and a Chromium browser.'] });
61
+ }
62
+ browser = await playwright.chromium.launch({ headless: true });
39
63
  }
40
- const initial = await fetchUrl(options.target, { timeoutMs: options.timeout || 30000, maxRetries: 1 });
41
- const browser = await playwright.chromium.launch({ headless: true });
42
64
  try {
43
- const capture = async (name, viewport, isMobile = false) => {
44
- const context = await browser.newContext({ viewport, isMobile });
45
- const page = await context.newPage();
46
- const failures = [];
47
- page.on('requestfailed', (request) => failures.push({ url: request.url(), error: request.failure()?.errorText || 'unknown' }));
48
- const response = await page.goto(options.target, { waitUntil: 'networkidle', timeout: options.timeout || 30000 });
49
- const html = await page.content(), text = await page.locator('body').innerText(), screenshot = await page.screenshot({ fullPage: true });
50
- const result = { name, final_url: page.url(), status: response?.status() ?? null, viewport, html, text, screenshot, failed_requests: failures };
51
- await context.close();
52
- return result;
53
- };
54
- const desktop = await capture('desktop', { width: 1280, height: 900 });
55
- const mobile = await capture('mobile', { width: 390, height: 844 }, true);
56
- const initialText = initial.body.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ').replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
57
- const data = {
58
- url: options.target, initial: { final_url: initial.url, status: initial.status, html_hash: sha256(initial.body), text_hash: sha256(initialText), word_count: words(initialText).length, redirect_chain: initial.redirectChain },
59
- desktop: { final_url: desktop.final_url, status: desktop.status, html_hash: sha256(desktop.html), text_hash: sha256(desktop.text), word_count: words(desktop.text).length, failed_requests: desktop.failed_requests },
60
- mobile: { final_url: mobile.final_url, status: mobile.status, html_hash: sha256(mobile.html), text_hash: sha256(mobile.text), word_count: words(mobile.text).length, failed_requests: mobile.failed_requests },
61
- parity: { initial_to_desktop_word_ratio: words(desktop.text).length ? Number((words(initialText).length / words(desktop.text).length).toFixed(3)) : null, mobile_to_desktop_word_ratio: words(desktop.text).length ? Number((words(mobile.text).length / words(desktop.text).length).toFixed(3)) : null },
62
- };
63
- const item = envelope('render', data, { method: 'browser', source: 'playwright/chromium', raw: `${initial.body}\n${desktop.html}\n${mobile.html}` });
64
- return observationRun(root, 'observe render', options.target, [item], { artifacts: { 'initial/response.html': initial.body, 'rendered/desktop-dom.html': desktop.html, 'rendered/desktop-text.txt': desktop.text, 'rendered/mobile-dom.html': mobile.html, 'rendered/mobile-text.txt': mobile.text, 'screenshots/desktop.png': desktop.screenshot, 'screenshots/mobile.png': mobile.screenshot } });
65
- } finally { await browser.close(); }
65
+ if (!capture) {
66
+ capture = async (name, viewport, { isMobile = false, javaScriptEnabled = true } = {}) => {
67
+ const context = await browser.newContext({ viewport, isMobile, javaScriptEnabled });
68
+ try {
69
+ const page = await context.newPage();
70
+ const failures = [];
71
+ page.on('requestfailed', (request) => failures.push({ url: request.url(), error: request.failure()?.errorText || 'unknown' }));
72
+ const response = await page.goto(options.target, { waitUntil: 'networkidle', timeout: options.timeout || 30000 });
73
+ const discovered = await page.locator('details > summary,[aria-expanded=false],[role=tab][aria-selected=false],button').evaluateAll((nodes) => nodes.map((node) => ({ tag: node.tagName.toLowerCase(), text: (node.textContent || '').trim().slice(0, 120), role: node.getAttribute('role'), expanded: node.getAttribute('aria-expanded') })).filter((item) => item.tag !== 'button' || /load more|show more|view more/i.test(item.text)));
74
+ const executed = [];
75
+ if (options.interactions) {
76
+ const controls = page.locator('details:not([open]) > summary,[aria-expanded=false],[role=tab][aria-selected=false],button');
77
+ for (let i = 0; i < Math.min(await controls.count(), 20); i++) { const control = controls.nth(i); const label = ((await control.textContent()) || '').trim(); if ((await control.evaluate((node) => node.tagName.toLowerCase())) === 'button' && !/load more|show more|view more/i.test(label)) continue; try { await control.click({ timeout: 2000 }); executed.push(label.slice(0, 120) || `control-${i + 1}`); } catch { executed.push(`failed:${label.slice(0, 100) || i + 1}`); } }
78
+ }
79
+ const html = await page.content(), text = await page.locator('body').innerText(), screenshot = await page.screenshot({ fullPage: true });
80
+ return { name, final_url: page.url(), status: response?.status() ?? null, viewport, javaScriptEnabled, html, text, screenshot, failed_requests: failures, interactions: { discovered, executed } };
81
+ } finally {
82
+ await context.close();
83
+ }
84
+ };
85
+ }
86
+ const initial = await (options.fetchUrl || fetchUrl)(options.target, { timeoutMs: options.timeout || 30000, maxRetries: 1 });
87
+ const initialText = visibleHtmlText(initial.body);
88
+ const observations = [...reused], artifacts = { 'initial/response.html': initial.body }, incomplete = [];
89
+ const profiles = [{ name: 'desktop', viewport: { width: 1280, height: 900 } }, { name: 'mobile', viewport: { width: 390, height: 844 }, settings: { isMobile: true } }, { name: 'javascript_disabled', viewport: { width: 1280, height: 900 }, settings: { javaScriptEnabled: false } }];
90
+ for (const profile of profiles.filter((item) => !reused.some((old) => old.data.profile === item.name))) {
91
+ try { const result = await capture(profile.name, profile.viewport, profile.settings); const count = words(result.text).length; const data = { url: options.target, profile: profile.name, final_url: result.final_url, status: result.status, viewport: result.viewport, javascript_enabled: result.javaScriptEnabled, interaction_execution_requested: Boolean(options.interactions), html_hash: sha256(result.html), text_hash: sha256(result.text), word_count: count, raw_http_word_ratio: count ? Number((words(initialText).length / count).toFixed(3)) : null, failed_requests: result.failed_requests, interactions: result.interactions }; observations.push(envelope('render', data, { method: 'browser', source: 'playwright/chromium', raw: result.html, limitations: result.interactions.executed.length ? ['Interactions were bounded to disclosure, tab, and load-more-like controls; application-specific journeys remain untested.'] : [] })); artifacts[`rendered/${profile.name}-dom.html`] = result.html; artifacts[`rendered/${profile.name}-text.txt`] = result.text; artifacts[`screenshots/${profile.name}.png`] = result.screenshot; }
92
+ catch (error) { incomplete.push(`${profile.name} render failed: ${error.message}`); observations.push(envelope('render', { url: options.target, profile: profile.name }, { method: 'browser', source: 'playwright/chromium', state: 'failed', confidence: 'confirmed', raw: `${profile.name}:${error.message}`, limitations: [error.message] })); }
93
+ }
94
+ const desktop = observations.find((item) => item.data.profile === 'desktop' && item.state === 'observed'), mobile = observations.find((item) => item.data.profile === 'mobile' && item.state === 'observed');
95
+ observations.push(envelope('render', { url: options.target, profile: 'parity', raw_http_to_desktop_word_ratio: desktop?.data.raw_http_word_ratio ?? null, mobile_to_desktop_word_ratio: desktop?.data.word_count && mobile?.data.word_count ? Number((mobile.data.word_count / desktop.data.word_count).toFixed(3)) : null, resumed_from_run_id: options.resumeRun || null }, { method: 'static_analysis', source: options.target, state: desktop && mobile ? 'observed' : 'incomplete', confidence: 'high', limitations: desktop && mobile ? [] : ['Desktop/mobile parity is incomplete because one or more profiles failed.'] }));
96
+ return observationRun(root, 'observe render', options.target, observations, { rawInputs: previousRaw ? { resumed_manifest: previousRaw } : {}, incomplete, warnings: reused.length ? [`${reused.length} successful profile(s) reused from immutable run ${options.resumeRun}; failed, absent, or configuration-mismatched profiles were recollected.`] : options.resumeRun ? [`No compatible successful profiles were reusable from immutable run ${options.resumeRun}; all profiles were recollected.`] : [], artifacts });
97
+ } finally { if (browser) await browser.close(); }
66
98
  }
67
99
 
68
100
  async function observeIndex(root, options) {
@@ -248,7 +280,75 @@ async function observeConsensus(root, options) {
248
280
  return observationRun(root, 'observe consensus', options.target, observations);
249
281
  }
250
282
 
283
+ const median = (values) => {
284
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
285
+ if (!sorted.length) return null;
286
+ const middle = Math.floor(sorted.length / 2);
287
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
288
+ };
289
+
290
+ function lighthouseMetrics(lhr) {
291
+ const audits = lhr.audits || {};
292
+ const value = (id) => Number.isFinite(audits[id]?.numericValue) ? audits[id].numericValue : null;
293
+ return {
294
+ performance_score: Number.isFinite(lhr.categories?.performance?.score) ? lhr.categories.performance.score : null,
295
+ first_contentful_paint_ms: value('first-contentful-paint'),
296
+ largest_contentful_paint_ms: value('largest-contentful-paint'),
297
+ cumulative_layout_shift: value('cumulative-layout-shift'),
298
+ total_blocking_time_ms: value('total-blocking-time'),
299
+ speed_index_ms: value('speed-index'),
300
+ };
301
+ }
302
+
303
+ async function localLighthouseRunner(target, runIndex, options) {
304
+ let lighthouse, launcher;
305
+ try {
306
+ ({ default: lighthouse } = await import('lighthouse'));
307
+ launcher = await import('chrome-launcher');
308
+ } catch {
309
+ throw new Error('optional lighthouse and chrome-launcher dependencies are not installed');
310
+ }
311
+ const chrome = await launcher.launch({ chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu'] });
312
+ try {
313
+ const result = await lighthouse(target, { port: chrome.port, output: 'json', logLevel: 'error', formFactor: options.deviceProfile || 'mobile' });
314
+ return result.lhr;
315
+ } finally {
316
+ await chrome.kill();
317
+ }
318
+ }
319
+
320
+ async function observeLighthouse(root, options) {
321
+ if (!/^https?:\/\//.test(options.target || '')) throw new Error('Lighthouse requires --target <http(s) URL>');
322
+ const repeat = Number.isInteger(options.repeat) && options.repeat >= 1 && options.repeat <= 5 ? options.repeat : 3;
323
+ const runner = options.lighthouseRunner || localLighthouseRunner;
324
+ const observations = [], artifacts = {}, incomplete = [];
325
+ for (let runIndex = 1; runIndex <= repeat; runIndex++) {
326
+ try {
327
+ const lhr = await runner(options.target, runIndex, options);
328
+ const metrics = lighthouseMetrics(lhr);
329
+ const data = {
330
+ url: options.target, provider: 'Lighthouse', evidence_type: 'lab', run_index: runIndex, runs_in_series: repeat,
331
+ lighthouse_version: lhr.lighthouseVersion || null, chrome_user_agent: lhr.userAgent || null, fetched_at: lhr.fetchTime || null,
332
+ final_url: lhr.finalDisplayedUrl || lhr.finalUrl || options.target,
333
+ configuration: { form_factor: lhr.configSettings?.formFactor || null, throttling_method: lhr.configSettings?.throttlingMethod || null, screen_emulation: lhr.configSettings?.screenEmulation || null, throttling: lhr.configSettings?.throttling || null },
334
+ metrics,
335
+ };
336
+ observations.push(envelope('performance', data, { method: 'browser', source: 'lighthouse/local', raw: JSON.stringify(lhr), limitations: ['This is controlled lab evidence, not field performance or a guarantee of user experience.'] }));
337
+ artifacts[`lighthouse/run-${String(runIndex).padStart(2, '0')}.json`] = JSON.stringify(lhr, null, 2);
338
+ } catch (error) {
339
+ incomplete.push(`Lighthouse run ${runIndex} failed: ${error.message}`);
340
+ observations.push(envelope('performance', { url: options.target, provider: 'Lighthouse', evidence_type: 'lab', run_index: runIndex, runs_in_series: repeat }, { method: 'browser', source: 'lighthouse/local', state: 'failed', confidence: 'confirmed', raw: `run-${runIndex}:${error.message}`, limitations: [error.message] }));
341
+ }
342
+ }
343
+ const successful = observations.filter((item) => item.state === 'observed');
344
+ const metricNames = ['performance_score', 'first_contentful_paint_ms', 'largest_contentful_paint_ms', 'cumulative_layout_shift', 'total_blocking_time_ms', 'speed_index_ms'];
345
+ const medians = Object.fromEntries(metricNames.map((name) => [name, median(successful.map((item) => item.data.metrics[name]))]));
346
+ observations.push(envelope('performance', { url: options.target, provider: 'Lighthouse', evidence_type: 'lab_summary', requested_runs: repeat, successful_runs: successful.length, median_metrics: medians }, { method: 'static_analysis', source: 'lighthouse/local', state: successful.length ? 'observed' : 'not_evidenced', confidence: successful.length === repeat ? 'high' : 'low', limitations: ['Medians summarize only successful controlled lab runs; failed runs remain separate evidence.'] }));
347
+ return observationRun(root, 'observe performance --lighthouse', options.target, observations, { incomplete, artifacts });
348
+ }
349
+
251
350
  async function observePerformance(root, options) {
351
+ if (options.lighthouse) return observeLighthouse(root, options);
252
352
  if (options.input) {
253
353
  const input = readInput(options.input);
254
354
  const rows = Array.isArray(input.value) ? input.value : [input.value];
@@ -279,6 +379,77 @@ function observeCorroboration(root, options) {
279
379
  return observationRun(root, 'observe corroboration', input.file, observations, { rawInputs: { corroboration_export: input.raw } });
280
380
  }
281
381
 
382
+ async function observeRepresentation(root, options) {
383
+ const input = readInput(options.input);
384
+ const manifest = input.value;
385
+ const integrity = verifyManifestIntegrity(manifest);
386
+ if (!integrity.ok) throw new Error(`release manifest is invalid: ${integrity.failures.join('; ')}`);
387
+ if (!options.target) throw new Error('representation observation requires --target <controlled surface URL>');
388
+ const surface = manifest.controlled_surfaces.find((item) => item.url === options.target || item.surface_id === options.surfaceId);
389
+ if (!surface) throw new Error('target is not a controlled surface in the release manifest');
390
+ const expected = manifest.projections.find((item) => item.projection_id === surface.projection_id);
391
+ if (!expected) throw new Error(`expected projection ${surface.projection_id} is missing`);
392
+ const collector = options.fetchUrl || fetchUrl;
393
+ const paths = [
394
+ { path: 'direct', url: surface.url },
395
+ { path: 'cache_busted', url: `${surface.url}${surface.url.includes('?') ? '&' : '?'}citable_manifest=${manifest.manifest_hash.slice(0, 16)}` },
396
+ ];
397
+ const observations = [];
398
+ const artifacts = {};
399
+ const incomplete = [];
400
+ for (const item of paths) {
401
+ try {
402
+ const response = await collector(item.url, { userAgent: options.userAgent || 'CitableRepresentationProbe/1.0', maxRetries: 1 });
403
+ const responseHash = sha256(response.body);
404
+ const observedProjectionHash = surface.verification_method === 'exact_response_body'
405
+ ? responseHash
406
+ : response.headers?.[surface.verification_header] || null;
407
+ const representationState = response.status >= 200 && response.status < 300
408
+ ? !observedProjectionHash ? 'insufficient_evidence' : observedProjectionHash === expected.sha256 ? 'consistent' : 'divergent'
409
+ : response.status === 403 || response.status === 429 ? 'challenged' : 'failed';
410
+ const data = {
411
+ release_id: manifest.release_id,
412
+ manifest_hash: manifest.manifest_hash,
413
+ surface_id: surface.surface_id,
414
+ projection_id: expected.projection_id,
415
+ retrieval_path: item.path,
416
+ requested_url: item.url,
417
+ final_url: response.url,
418
+ status: response.status,
419
+ redirect_chain: response.redirectChain || [],
420
+ headers: response.headers || {},
421
+ verification_method: surface.verification_method,
422
+ verification_header: surface.verification_header,
423
+ expected_projection_hash: expected.sha256,
424
+ observed_projection_hash: observedProjectionHash,
425
+ observed_response_hash: responseHash,
426
+ representation_state: representationState,
427
+ authority_label: 'external_unverified',
428
+ gates_release_finalization: false,
429
+ request_identity: options.userAgent || 'CitableRepresentationProbe/1.0',
430
+ region: options.region || 'local_unspecified',
431
+ };
432
+ observations.push(envelope('representation_drift', data, {
433
+ method: 'synthetic_fetch', source: item.url, raw: response.body,
434
+ confidence: 'confirmed',
435
+ limitations: ['One synthetic retrieval does not represent all caches, indexes, products, accounts, geographies, or users.'],
436
+ authority: { source_authority: 'unknown', collection_authority: 'synthetic_probe', authenticity_status: 'unverified', representativeness: 'single_observation' },
437
+ }));
438
+ artifacts[`representation/${item.path}-response.txt`] = response.body;
439
+ artifacts[`representation/${item.path}-headers.json`] = response.headers || {};
440
+ } catch (error) {
441
+ incomplete.push(`${item.path} representation retrieval failed: ${error.message}`);
442
+ observations.push(envelope('representation_drift', {
443
+ release_id: manifest.release_id, manifest_hash: manifest.manifest_hash, surface_id: surface.surface_id,
444
+ projection_id: expected.projection_id, retrieval_path: item.path, requested_url: item.url,
445
+ representation_state: 'failed', authority_label: 'external_unverified', gates_release_finalization: false,
446
+ request_identity: options.userAgent || 'CitableRepresentationProbe/1.0', region: options.region || 'local_unspecified',
447
+ }, { method: 'synthetic_fetch', source: item.url, state: 'failed', confidence: 'confirmed', raw: error.message, limitations: [error.message], authority: { source_authority: 'unknown', collection_authority: 'synthetic_probe', authenticity_status: 'unverified', representativeness: 'single_observation' } }));
448
+ }
449
+ }
450
+ return observationRun(root, 'observe representation', surface.url, observations, { rawInputs: { release_manifest: input.raw }, incomplete, artifacts });
451
+ }
452
+
282
453
  export async function observe(root, mode, options = {}) {
283
454
  switch (mode) {
284
455
  case 'render': return observeRender(root, options);
@@ -291,6 +462,7 @@ export async function observe(root, mode, options = {}) {
291
462
  case 'performance': return observePerformance(root, options);
292
463
  case 'corroboration': return observeCorroboration(root, options);
293
464
  case 'media': return observeMedia(root, options);
294
- default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, corroboration, or media');
465
+ case 'representation': return observeRepresentation(root, options);
466
+ default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, corroboration, media, or representation');
295
467
  }
296
468
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * outcomes command — Customer Outcomes registry
3
+ *
4
+ * Enforces the finding_produced → causal_relationship_established
5
+ * progression and refuses to conflate activity with validated impact.
6
+ *
7
+ * Usage:
8
+ * citable outcomes list [--stage <stage>]
9
+ * citable outcomes show <outcome_id>
10
+ * citable outcomes validate
11
+ * citable outcomes summary — stage distribution summary
12
+ */
13
+ import { contextDir, loadRegistryFile, registryLoadProblems } from '../registries/index.js';
14
+ import { validateAgainst } from '../shared/schemaValidator.js';
15
+ import path from 'node:path';
16
+
17
+
18
+ export const OUTCOME_STAGES = [
19
+ 'finding_produced',
20
+ 'finding_accepted',
21
+ 'finding_remediated',
22
+ 'remediation_validated',
23
+ 'search_result_changed',
24
+ 'answer_engine_changed',
25
+ 'commercial_outcome_changed',
26
+ 'causal_relationship_established',
27
+ ];
28
+
29
+ export async function outcomesCommand(args, root = process.cwd()) {
30
+ const [subcommand, ...rest] = args;
31
+ const file = path.join(contextDir(root), 'customer-outcomes.yaml');
32
+
33
+ switch (subcommand) {
34
+ case 'show': return outcomeShow(file, rest[0]);
35
+ case 'validate': return outcomeValidate(file);
36
+ case 'summary': return outcomeSummary(file);
37
+ default: return outcomeList(file, rest);
38
+ }
39
+ }
40
+
41
+ function load(file) {
42
+ return loadRegistryFile(file, 'customer-outcomes');
43
+ }
44
+
45
+ function outcomeList(file, args) {
46
+ const data = load(file);
47
+ const stageFilter = args.find((_, i, a) => a[i-1] === '--stage');
48
+ let entries = stageFilter
49
+ ? data.entries.filter(e => e.outcome_stage === stageFilter)
50
+ : data.entries;
51
+ return {
52
+ outcomes: entries.map(e => ({
53
+ outcome_id: e.outcome_id,
54
+ customer: e.customer,
55
+ property: e.property,
56
+ outcome_stage: e.outcome_stage,
57
+ causal_confidence: e.causal_confidence,
58
+ customer_confirmed: e.customer_confirmed,
59
+ independently_verified: e.independently_verified ?? false,
60
+ })),
61
+ total: entries.length,
62
+ };
63
+ }
64
+
65
+ function outcomeShow(file, id) {
66
+ if (!id) return { error: 'outcome_id required' };
67
+ const data = load(file);
68
+ const o = data.entries.find(e => e.outcome_id === id);
69
+ if (!o) return { error: `Outcome not found: ${id}` };
70
+ return { outcome: o };
71
+ }
72
+
73
+ function outcomeSummary(file) {
74
+ const data = load(file);
75
+ const dist = {};
76
+ for (const s of OUTCOME_STAGES) dist[s] = 0;
77
+ for (const e of data.entries) dist[e.outcome_stage] = (dist[e.outcome_stage] ?? 0) + 1;
78
+ const verified = data.entries.filter(e => e.independently_verified).length;
79
+ const confirmed = data.entries.filter(e => e.customer_confirmed).length;
80
+ return {
81
+ stage_distribution: dist,
82
+ customer_confirmed: confirmed,
83
+ independently_verified: verified,
84
+ total: data.entries.length,
85
+ warning: dist.finding_produced > 0 && dist.causal_relationship_established === 0
86
+ ? 'All outcomes at finding_produced stage — no causal relationships established yet'
87
+ : null,
88
+ };
89
+ }
90
+
91
+ function outcomeValidate(file) {
92
+ const data = load(file);
93
+ const problems = [...registryLoadProblems(data)];
94
+ const { valid, errors } = validateAgainst('customer-outcome.schema.json', data);
95
+ if (!valid) problems.push(...errors);
96
+
97
+ for (const o of data.entries) {
98
+ // Cannot claim causal confidence without evidence
99
+ if (['strong','verified'].includes(o.causal_confidence) && (!o.evidence_package || o.evidence_package.length === 0))
100
+ problems.push(`${o.outcome_id}: causal_confidence "${o.causal_confidence}" requires evidence_package`);
101
+ // Cannot mark independently_verified without customer_confirmed first
102
+ if (o.independently_verified && !o.customer_confirmed)
103
+ problems.push(`${o.outcome_id}: independently_verified=true but customer_confirmed=false`);
104
+ // Commercial value requires validated stage
105
+ if (o.commercial_value && !['commercial_outcome_changed','causal_relationship_established'].includes(o.outcome_stage))
106
+ problems.push(`${o.outcome_id}: commercial_value set but outcome_stage is "${o.outcome_stage}" — commercial value requires commercial_outcome_changed or causal_relationship_established`);
107
+ // Publication requires confirmation
108
+ if (o.publication_permission && !o.customer_confirmed)
109
+ problems.push(`${o.outcome_id}: publication_permission=true but customer_confirmed=false`);
110
+ }
111
+
112
+ return { valid: problems.length === 0, problems, checked: data.entries.length };
113
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * prioritize command — Initiative Prioritization
3
+ *
4
+ * Ranks roadmap and investment choices. Weights must be visible when
5
+ * weighted scoring is used — no opaque scores.
6
+ *
7
+ * Usage:
8
+ * citable prioritize [--status proposed|approved]
9
+ * citable prioritize show <initiative_id>
10
+ * citable prioritize validate
11
+ * citable prioritize rank — ranked list with explicit criteria
12
+ */
13
+ import { contextDir, loadRegistryFile, registryLoadProblems } from '../registries/index.js';
14
+ import { validateAgainst } from '../shared/schemaValidator.js';
15
+ import path from 'node:path';
16
+
17
+
18
+ const SCALE = { none: 0, low: 1, medium: 2, high: 3, critical: 4, validated: 4, transformative: 4, trivial: 0, very_high: 4, negligible: 0, 'n/a': 0 };
19
+
20
+ export async function prioritizeCommand(args, root = process.cwd()) {
21
+ const [subcommand, ...rest] = args;
22
+ const file = path.join(contextDir(root), 'initiatives.yaml');
23
+
24
+ switch (subcommand) {
25
+ case 'show': return initiativeShow(file, rest[0]);
26
+ case 'validate': return initiativeValidate(file);
27
+ case 'rank': return initiativeRank(file);
28
+ case 'list':
29
+ default: {
30
+ const statusFilter = rest[rest.indexOf('--status') + 1] ?? null;
31
+ return initiativeList(file, statusFilter);
32
+ }
33
+ }
34
+ }
35
+
36
+ function load(file) {
37
+ return loadRegistryFile(file, 'initiatives');
38
+ }
39
+
40
+ function initiativeList(file, statusFilter) {
41
+ const data = load(file);
42
+ let entries = statusFilter ? data.entries.filter(i => i.status === statusFilter) : data.entries;
43
+ return {
44
+ initiatives: entries.map(i => ({
45
+ initiative_id: i.initiative_id,
46
+ title: i.title,
47
+ customer_demand: i.customer_demand,
48
+ revenue_potential: i.revenue_potential,
49
+ strategic_differentiation: i.strategic_differentiation,
50
+ evidence_strength: i.evidence_strength,
51
+ engineering_cost: i.engineering_cost,
52
+ reversibility: i.reversibility,
53
+ status: i.status,
54
+ owner: i.owner,
55
+ })),
56
+ total: entries.length,
57
+ };
58
+ }
59
+
60
+ function initiativeShow(file, id) {
61
+ if (!id) return { error: 'initiative_id required' };
62
+ const data = load(file);
63
+ const i = data.entries.find(e => e.initiative_id === id);
64
+ if (!i) return { error: `Initiative not found: ${id}` };
65
+ return { initiative: i };
66
+ }
67
+
68
+ function initiativeRank(file) {
69
+ const data = load(file);
70
+ const scorable = data.entries.filter(i => ['proposed','approved'].includes(i.status));
71
+
72
+ // Transparent scoring: benefit - cost, with evidence strength as multiplier
73
+ const scored = scorable.map(i => {
74
+ const benefit = (SCALE[i.customer_demand] ?? 0) + (SCALE[i.revenue_potential] ?? 0) + (SCALE[i.strategic_differentiation] ?? 0);
75
+ const cost = (SCALE[i.engineering_cost] ?? 0) + (SCALE[i.operating_cost ?? 'none'] ?? 0) + (SCALE[i.legal_risk ?? 'none'] ?? 0);
76
+ const evidenceMultiplier = { assumption: 0.5, anecdote: 0.6, proxy: 0.7, validated: 0.9, verified: 1.0 }[i.evidence_strength] ?? 0.5;
77
+ const score = (benefit - cost) * evidenceMultiplier;
78
+ return { initiative_id: i.initiative_id, title: i.title, score: Math.round(score * 10) / 10, benefit, cost, evidence_strength: i.evidence_strength, reversibility: i.reversibility, evidence_multiplier: evidenceMultiplier };
79
+ }).sort((a, b) => b.score - a.score);
80
+
81
+ return {
82
+ ranked: scored,
83
+ scoring_method: 'transparent: (customer_demand + revenue_potential + strategic_differentiation - engineering_cost - operating_cost - legal_risk) × evidence_multiplier',
84
+ weights: 'equal weights — override with scoring_weights field per initiative if asymmetric weighting is required',
85
+ total: scored.length,
86
+ };
87
+ }
88
+
89
+ function initiativeValidate(file) {
90
+ const data = load(file);
91
+ const problems = [...registryLoadProblems(data)];
92
+ const { valid, errors } = validateAgainst('initiative.schema.json', data);
93
+ if (!valid) problems.push(...errors);
94
+
95
+ for (const i of data.entries) {
96
+ // Evidence strength required
97
+ if (i.evidence_strength === 'assumption' && i.revenue_potential === 'transformative')
98
+ problems.push(`${i.initiative_id}: revenue_potential=transformative with evidence_strength=assumption — requires at least "proxy" evidence`);
99
+ // One-way decisions need justification
100
+ if (i.reversibility === 'effectively_one_way' && !i.opportunity_cost)
101
+ problems.push(`${i.initiative_id}: effectively_one_way initiatives require opportunity_cost`);
102
+ // Platform dependency documented for platform-dependent initiatives
103
+ if (!i.platform_dependency && /search.console|bing|openai|google|cloudflare/i.test(i.title))
104
+ problems.push(`${i.initiative_id}: platform_dependency should be documented for platform-dependent initiatives`);
105
+ }
106
+
107
+ return { valid: problems.length === 0, problems, checked: data.entries.length };
108
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * risk command — Risk Register
3
+ *
4
+ * Usage:
5
+ * citable risk list [--board] List all risks (--board: board-visible only)
6
+ * citable risk show <risk_id>
7
+ * citable risk validate
8
+ * citable risk top Top risks by residual_exposure
9
+ */
10
+ import { contextDir, loadRegistryFile, registryLoadProblems } from '../registries/index.js';
11
+ import { validateAgainst } from '../shared/schemaValidator.js';
12
+ import path from 'node:path';
13
+
14
+ import { dateOnly, parseAsOf } from '../shared/asOf.js';
15
+
16
+ const EXPOSURE_ORDER = ['low','medium','high','critical'];
17
+
18
+ export async function riskCommand(args, root = process.cwd()) {
19
+ const [subcommand, ...rest] = args;
20
+ const file = path.join(contextDir(root), 'risks.yaml');
21
+
22
+ switch (subcommand) {
23
+ case 'show': return riskShow(file, rest[0]);
24
+ case 'validate': return riskValidate(file, parseAsOf(args));
25
+ case 'top': return riskTop(file);
26
+ case 'list':
27
+ default:
28
+ return riskList(file, { boardOnly: rest.includes('--board') });
29
+ }
30
+ }
31
+
32
+ function load(file) {
33
+ return loadRegistryFile(file, 'risks');
34
+ }
35
+
36
+ function riskList(file, { boardOnly = false } = {}) {
37
+ const data = load(file);
38
+ let entries = boardOnly ? data.entries.filter(r => r.board_visibility) : data.entries;
39
+ return {
40
+ risks: entries.map(r => ({
41
+ risk_id: r.risk_id,
42
+ title: r.title,
43
+ category: r.category,
44
+ gross_exposure: r.gross_exposure,
45
+ residual_exposure:r.residual_exposure,
46
+ trend: r.trend,
47
+ review_date: r.review_date,
48
+ board_visibility: r.board_visibility,
49
+ })),
50
+ total: entries.length,
51
+ };
52
+ }
53
+
54
+ function riskShow(file, id) {
55
+ if (!id) return { error: 'risk_id required' };
56
+ const data = load(file);
57
+ const r = data.entries.find(e => e.risk_id === id);
58
+ if (!r) return { error: `Risk not found: ${id}` };
59
+ return { risk: r };
60
+ }
61
+
62
+ function riskTop(file) {
63
+ const data = load(file);
64
+ const sorted = [...data.entries].sort((a, b) =>
65
+ EXPOSURE_ORDER.indexOf(b.residual_exposure) - EXPOSURE_ORDER.indexOf(a.residual_exposure)
66
+ );
67
+ return {
68
+ top_risks: sorted.slice(0, 10).map(r => ({
69
+ risk_id: r.risk_id,
70
+ title: r.title,
71
+ category: r.category,
72
+ residual_exposure: r.residual_exposure,
73
+ trend: r.trend,
74
+ control_effectiveness: r.control_effectiveness,
75
+ trigger_threshold: r.trigger_threshold,
76
+ response_owner: r.response_owner,
77
+ })),
78
+ };
79
+ }
80
+
81
+ function riskValidate(file, asOf) {
82
+ const data = load(file);
83
+ const problems = [...registryLoadProblems(data)];
84
+ const { valid, errors } = validateAgainst('risk.schema.json', data);
85
+ if (!valid) problems.push(...errors);
86
+
87
+ for (const r of data.entries) {
88
+ // KRIs required for critical/high risks
89
+ if (['high','critical'].includes(r.residual_exposure) && (!r.key_risk_indicators || r.key_risk_indicators.length === 0))
90
+ problems.push(`${r.risk_id}: high/critical residual_exposure requires key_risk_indicators`);
91
+ // Trigger threshold required for board-visible risks
92
+ if (r.board_visibility && !r.trigger_threshold)
93
+ problems.push(`${r.risk_id}: board_visibility=true requires trigger_threshold`);
94
+ // Stale review dates
95
+ const reviewDate = r.review_date ? new Date(r.review_date) : null;
96
+ if (reviewDate && reviewDate < asOf)
97
+ problems.push(`${r.risk_id}: review_date ${r.review_date} is in the past — risk review overdue`);
98
+ // Controls effectiveness vs gross/residual mismatch
99
+ if (r.gross_exposure === 'critical' && r.control_effectiveness === 'none' && r.residual_exposure !== 'critical')
100
+ problems.push(`${r.risk_id}: control_effectiveness=none but residual_exposure < gross_exposure — inconsistent`);
101
+ }
102
+
103
+ return { valid: problems.length === 0, problems, checked: data.entries.length, as_of: dateOnly(asOf) };
104
+ }