@nebulacomponents/citable 1.15.1 → 1.17.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 (264) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +18 -5
  3. package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
  6. package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
  7. package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
  8. package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
  9. package/dist/universal/.agents/skills/citable/manifest.json +17 -12
  10. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  11. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
  12. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  13. package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
  14. package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
  15. package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  16. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  17. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  18. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  19. package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
  20. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  21. package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
  22. package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
  23. package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
  24. package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
  25. package/dist/universal/.claude/skills/citable/manifest.json +17 -12
  26. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  27. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
  28. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  29. package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
  30. package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
  31. package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  32. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  33. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  34. package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
  35. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  36. package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
  37. package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
  38. package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
  39. package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
  40. package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
  41. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  42. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
  43. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
  45. package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
  46. package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  47. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  48. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  49. package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
  50. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  51. package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
  52. package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
  53. package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
  54. package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
  55. package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
  56. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
  58. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  59. package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
  60. package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
  61. package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  62. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  63. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  64. package/dist/universal/.github/skills/citable/SKILL.md +51 -1
  65. package/dist/universal/.github/skills/citable/VERSION +1 -1
  66. package/dist/universal/.github/skills/citable/commands/README.md +3 -1
  67. package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
  68. package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
  69. package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
  70. package/dist/universal/.github/skills/citable/manifest.json +17 -12
  71. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  72. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
  73. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  74. package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
  75. package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
  76. package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  77. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  78. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  79. package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
  80. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  81. package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
  82. package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
  83. package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
  84. package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
  85. package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
  86. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  87. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
  88. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  89. package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
  90. package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
  91. package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  92. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  93. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  94. package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
  95. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  96. package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
  97. package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
  98. package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
  99. package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
  100. package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
  101. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  102. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
  103. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  104. package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
  105. package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
  106. package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  107. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  108. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  109. package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
  110. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  111. package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
  112. package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
  113. package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
  114. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
  115. package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
  116. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  117. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
  118. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  119. package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
  120. package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
  121. package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  122. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  123. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  124. package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
  125. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  126. package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
  127. package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
  128. package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
  129. package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
  130. package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
  131. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  132. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
  133. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  134. package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
  135. package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
  136. package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  137. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  138. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  139. package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
  140. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  141. package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
  142. package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
  143. package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
  144. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
  145. package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
  146. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  147. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
  148. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  149. package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
  150. package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
  151. package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  152. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  153. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  154. package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
  155. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  156. package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
  157. package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
  158. package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
  159. package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
  160. package/dist/universal/.trae/skills/citable/manifest.json +17 -12
  161. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  162. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
  163. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  164. package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
  165. package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
  166. package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  167. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  168. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  169. package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
  170. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  171. package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
  172. package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
  173. package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
  174. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
  175. package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
  176. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  177. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
  178. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  179. package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
  180. package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
  181. package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  182. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  183. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  184. package/dist/universal/manifest.json +208 -148
  185. package/package.json +1 -1
  186. package/schemas/cli-output-envelope.schema.json +30 -0
  187. package/schemas/experiment.schema.json +1 -1
  188. package/schemas/finding.schema.json +69 -0
  189. package/schemas/fleet.schema.json +93 -0
  190. package/schemas/funnel.schema.json +116 -0
  191. package/schemas/mcp-transport-envelope.schema.json +244 -0
  192. package/schemas/observation.schema.json +1 -1
  193. package/schemas/remediation-verification.schema.json +98 -0
  194. package/skill/SKILL.md +51 -1
  195. package/skill/commands/README.md +3 -1
  196. package/skill/commands/governance.md +12 -0
  197. package/skill/commands/measurement.md +20 -0
  198. package/skill/commands/reporting.md +16 -0
  199. package/src/analysis/saliency.js +119 -0
  200. package/src/cli/index.js +651 -13
  201. package/src/commands/attributeImpact.js +54 -0
  202. package/src/commands/audit.js +2 -2
  203. package/src/commands/ciWorkflow.js +63 -0
  204. package/src/commands/compatibility.js +205 -0
  205. package/src/commands/connect.js +3 -0
  206. package/src/commands/context.js +2 -1
  207. package/src/commands/edgeRules.js +196 -0
  208. package/src/commands/edgeSecurity.js +192 -0
  209. package/src/commands/exceptions.js +241 -0
  210. package/src/commands/experimentGuardrails.js +213 -0
  211. package/src/commands/fleet.js +104 -0
  212. package/src/commands/generateLlmsTxt.js +111 -0
  213. package/src/commands/goldenCorpus.js +155 -0
  214. package/src/commands/implementationKit.js +202 -0
  215. package/src/commands/inspectAeo.js +74 -0
  216. package/src/commands/inspectCro.js +142 -0
  217. package/src/commands/inspectGeo.js +52 -0
  218. package/src/commands/inspectSerp.js +317 -0
  219. package/src/commands/lintComponents.js +132 -0
  220. package/src/commands/monitor.js +138 -0
  221. package/src/commands/observe.js +6 -1
  222. package/src/commands/planExperiment.js +120 -0
  223. package/src/commands/previewCro.js +241 -0
  224. package/src/commands/probe.js +70 -0
  225. package/src/commands/remediate.js +461 -0
  226. package/src/commands/reportConsensus.js +48 -0
  227. package/src/commands/testFunnel.js +107 -0
  228. package/src/commands/verifyRemediation.js +204 -0
  229. package/src/commands/visualRegression.js +165 -0
  230. package/src/components/index.js +556 -0
  231. package/src/connectors/index.js +6 -1
  232. package/src/connectors/indexnow.js +431 -0
  233. package/src/connectors/mcp/allowlist.js +107 -0
  234. package/src/connectors/mcp/client.js +239 -0
  235. package/src/connectors/mcp/diagnostics.js +214 -0
  236. package/src/connectors/mcp/gscServer.js +155 -0
  237. package/src/connectors/mcp/http.js +130 -0
  238. package/src/connectors/mcp/index.js +7 -0
  239. package/src/connectors/mcp/pilot.js +138 -0
  240. package/src/connectors/mcp/pilotServer.js +80 -0
  241. package/src/connectors/mcp/sanitizer.js +38 -0
  242. package/src/connectors/mcp/stdio.js +178 -0
  243. package/src/crawler/ipRanges.js +161 -0
  244. package/src/detectors/agent.js +127 -0
  245. package/src/detectors/ans.js +282 -0
  246. package/src/detectors/crawl.js +46 -0
  247. package/src/detectors/cro.js +825 -0
  248. package/src/detectors/cwv.js +38 -1
  249. package/src/detectors/evd.js +101 -0
  250. package/src/detectors/framework.js +32 -2
  251. package/src/detectors/geoReco.js +408 -0
  252. package/src/detectors/index.js +4 -2
  253. package/src/detectors/link.js +112 -1
  254. package/src/detectors/page.js +101 -0
  255. package/src/detectors/schemaData.js +529 -0
  256. package/src/detectors/tech.js +306 -0
  257. package/src/extractor/page.js +186 -2
  258. package/src/extractor/site.js +9 -2
  259. package/src/installer/index.js +45 -2
  260. package/src/monitoring/alertDelivery.js +101 -7
  261. package/src/observations/attribution.js +217 -0
  262. package/src/registries/index.js +14 -0
  263. package/src/reporting/consensus.js +416 -0
  264. package/src/reporting/executiveExport.js +244 -0
package/src/cli/index.js CHANGED
@@ -1,14 +1,36 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import { init } from '../commands/init.js';
5
6
  import { audit } from '../commands/audit.js';
7
+ import { auditEdgeCode } from '../commands/edgeSecurity.js';
6
8
  import { planAudit } from '../commands/planAudit.js';
7
9
  import { demo } from '../commands/demo.js';
8
10
  import { validate } from '../commands/validate.js';
9
11
  import { mapClaims } from '../commands/mapClaims.js';
10
12
  import { substantiate } from '../commands/substantiate.js';
11
13
  import { inspect } from '../commands/inspect.js';
14
+ import { inspectSerp } from '../commands/inspectSerp.js';
15
+ import { inspectCro } from '../commands/inspectCro.js';
16
+ import { inspectAeo } from '../commands/inspectAeo.js';
17
+ import { inspectGeo } from '../commands/inspectGeo.js';
18
+ import { testFunnel } from '../commands/testFunnel.js';
19
+ import { planExperiment } from '../commands/planExperiment.js';
20
+ import { fleetSummary, fleetAudit } from '../commands/fleet.js';
21
+ import { exportEdgeRules, testEdgeRules } from '../commands/edgeRules.js';
22
+ import { probeEngine } from '../commands/probe.js';
23
+ import { lintComponents } from '../commands/lintComponents.js';
24
+ import { exportExecutiveReport } from '../reporting/executiveExport.js';
25
+ import { attributeImpact } from '../commands/attributeImpact.js';
26
+ import { remediateCommand } from '../commands/remediate.js';
27
+ import { verifyRemediation } from '../commands/verifyRemediation.js';
28
+ import { exportImplementationKit } from '../commands/implementationKit.js';
29
+ import { compatibilityCommand, verifyPage } from '../commands/compatibility.js';
30
+ import { runVisualRegression } from '../commands/visualRegression.js';
31
+ import { checkExperiment } from '../commands/experimentGuardrails.js';
32
+ import { runGoldenBenchmark } from '../commands/goldenCorpus.js';
33
+ import { previewCroCommand } from '../commands/previewCro.js';
12
34
  import { schemaCommand } from '../commands/schemaCmd.js';
13
35
  import { compareSnapshots } from '../commands/compareSnapshots.js';
14
36
  import { isInstallerCommand, runInstallerCommand } from '../installer/index.js';
@@ -19,9 +41,12 @@ import { applyRemediation } from '../commands/applyRemediation.js';
19
41
  import { monitor, monitorAndAlert } from '../commands/monitor.js';
20
42
  import { reportDashboard } from '../commands/reportDashboard.js';
21
43
  import { reportShareOfVoice } from '../commands/reportShareOfVoice.js';
44
+ import { reportConsensus } from '../commands/reportConsensus.js';
22
45
  import { evaluateObjective, importMetrics, initializeObjective, validateObjectives } from '../commands/measurement.js';
23
- import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection, readCmsContent, applyCmsRemediation } from '../commands/connect.js';
46
+ import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection, readCmsContent, applyCmsRemediation, submitIndexNow, collectMcpEvidence } from '../commands/connect.js';
47
+ import { generateLlmsTxt } from '../commands/generateLlmsTxt.js';
24
48
  import { evaluateDispositions, validateGovernance } from '../commands/governance.js';
49
+ import { listExceptions, renewException, invalidateException } from '../commands/exceptions.js';
25
50
  import { evaluateReviews, initializeSamplingPlan, prioritizeReviews, queueReviews, selectSample } from '../commands/reviews.js';
26
51
  import { selfUpgradeCommand, selfUpgradeExitCode } from '../commands/selfUpgrade.js';
27
52
  // Executive reporting suite
@@ -52,13 +77,24 @@ Commands
52
77
  check Report installed and available Citable versions
53
78
  uninstall Remove managed Citable skill files
54
79
  list List detected providers and installation state
55
- doctor Diagnose installer, provider, and integrity problems
80
+ doctor [--mcp] Diagnose installer, provider, MCP transport, and integrity problems
56
81
  init [--seed <name>] Initialize .citable/ context and registries (non-destructive)
57
82
  demo Run the detector engine against a bundled offline example (no setup, no network)
58
83
  audit [scope] Run detectors; scopes: technical seo aeo geo architecture entity
59
84
  claims evidence schema lifecycle corroboration
60
85
  plan-audit Inspect target signals and propose an evidence-bounded audit plan
61
86
  inspect <page> Profile one page (URL path or source file)
87
+ inspect serp <page> Simulate Google SERP title/snippet truncation and rich results
88
+ inspect cro <page> Evaluate page conversion readiness, CTA visibility, and friction
89
+ inspect saliency <page> Evaluate privacy-first algorithmic visual attention and CTA conspicuity
90
+ inspect aeo <page> Evaluate answer extractability, question density, and structure
91
+ inspect geo <page> Evaluate RAG chunkability, section density, and retrieval posture
92
+ preview cro <page> Interactive split-screen visual preview of original vs remediated page
93
+ remediate [finding] Synthesize accessible Nebula Components; safe patches with diff, validation, confidence, rollback (--write gated, fail closed)
94
+ test funnel [id] Test multi-step conversion funnel continuity and parameter persistence
95
+ test visual Deterministic layout-contract checks + viewport/variant screenshot matrix (screenshots need playwright)
96
+ plan experiment Calculate statistical sample size and duration for A/B testing
97
+ check experiment Experiment guardrails: SRM, stopping, power, contamination, lifecycle status
62
98
  map-claims Extract material claim candidates from pages (--write to save)
63
99
  substantiate Assess claim/evidence status (--write to apply downgrades)
64
100
  schema Validate deployed JSON-LD and propose registry-derived schema
@@ -69,9 +105,10 @@ Commands
69
105
  consensus, performance, corroboration, crawler or regional probes,
70
106
  media evidence, or representation evidence
71
107
  apply Apply a reviewed, hash-locked remediation spec
72
- monitor [runA runB] Compare observation runs and emit regression alerts [--webhook <url>]
108
+ monitor [runA runB] Compare observation runs and emit regression alerts [--webhook <url>] [--min-severity <level>]
73
109
  report dashboard [--last N] [--since <run-id>] Render a cross-run evidence trend as Markdown + HTML
74
110
  report share-of-voice [--last N] Compute first-party and competitor citation share
111
+ report consensus [--last N] [--since <run-id>] Render Canonical Discovery Consensus Matrix (Markdown + HTML)
75
112
  metrics import Import declared metric observations from CSV/JSON
76
113
  connect status List optional connectors and configured connections
77
114
  connect configure Configure non-secret connection state (--write to save)
@@ -81,11 +118,16 @@ Commands
81
118
  connect read Read CMS content and hash for remediation targeting
82
119
  connect apply Apply reviewed, hash-locked CMS remediation (--write to apply)
83
120
  connect disconnect Remove optional connection state (--write to confirm)
121
+ connect indexnow Submit changed or discovered URLs to IndexNow engines (--write to submit)
122
+ connect mcp Collect evidence via allowlisted read-only MCP transport adapter
84
123
  objectives init Validate/add one objective from --input (--write to save)
85
124
  objectives validate Validate objective contracts and metric references
86
125
  evaluate [objective-id] Compare objective baseline and evaluation windows
87
126
  governance validate Validate reviewer, policy, and exception controls
88
127
  governance evaluate [run] Produce immutable enforcement dispositions without changing findings
128
+ exceptions list List governed exceptions with expiry status [--expired]
129
+ exceptions renew Renew governed exception with reviewer/evidence controls (--write to save)
130
+ exceptions invalidate Revoke governed exception with mandatory audit reason (--write to save)
89
131
  reviews queue [run policy] Create semantic review work from heuristic findings
90
132
  reviews prioritize Rank review work from explicit materiality inputs
91
133
  reviews plan Validate/add a sampling plan from --input
@@ -96,9 +138,14 @@ Commands
96
138
  corpus evaluate Evaluate a disclosed real-property acceptance corpus
97
139
  corpus publish Validate and project an owner-authorized public corpus
98
140
  corpus receipt Create a reproducibility receipt for a sealed run
141
+ corpus benchmark Run the labeled golden fixture corpus and report per-detector precision/recall
99
142
  corpus compare-receipts Compare two acceptance-run receipt envelopes
100
143
  artifacts export Export one sealed run as a portable verified directory
101
144
  artifacts verify Verify an exported artifact interchange directory
145
+ verify remediation Closed loop: re-run the detector after a patch and emit a before/after evidence bundle
146
+ verify page <page> Run all detectors scoped to one page and report posture (pass/attention/blocked)
147
+ compatibility Pre-flight: Node engine, optional adapters, browser, framework, registries, edge limits
148
+ kit export Customer-ready implementation kit: finding, diff, evidence, acceptance tests, deployment
102
149
  artifacts import Import a verified run without changing its canonical bytes
103
150
  self-upgrade Check for a newer version and upgrade the npx cache
104
151
  kpi [list|show|validate] KPI architecture — govern metric definitions, sources, targets
@@ -141,6 +188,23 @@ Options
141
188
  --lighthouse Run local, repeated Lighthouse lab observations
142
189
  --ocr Explicitly request optional OCR for media images
143
190
  --seed <name> Overlay a bundled starter registry seed (all entries default to unverified)
191
+ --urls <file|list> URL list or file for IndexNow submission
192
+ --sitemap <url|file> Sitemap URL or file for URL discovery and submission
193
+ --host <hostname> Target host for IndexNow submission
194
+ --key <key> IndexNow key (or INDEXNOW_KEY environment variable)
195
+ --key-location <url> Custom location of IndexNow key file
196
+ --skip-key-verify Skip pre-flight key verification HTTP probe
197
+ --server <id> Allowlisted MCP server identifier
198
+ --tool <name> Allowlisted read-only MCP tool name
199
+ --transport <stdio|http> MCP transport protocol (stdio or http)
200
+ --server-script <path> Local script path for stdio MCP server
201
+ --tool-args <json> JSON arguments string for MCP tool call
202
+ --until <YYYY-MM-DD> Target expiration date for exception renewal
203
+ --reason <text> Required justification for exception invalidation
204
+ --evidence <id> Evidence ID to link to exception renewal
205
+ --note <text> Optional note for exception renewal audit trail
206
+ --expired Filter exception list to expired records only
207
+ --expiring-soon <days> Threshold days to flag expiring exceptions (default: 14)
144
208
  --write Persist registry changes (map-claims, substantiate)
145
209
  --json Machine-readable output only
146
210
 
@@ -159,6 +223,7 @@ function parseArgs(argv) {
159
223
  else if (a === '--target') args.target = argv[++i];
160
224
  else if (a === '--base-url') args.baseUrl = argv[++i];
161
225
  else if (a === '--ref-date') args.refDate = argv[++i];
226
+ else if (a === '--viewport') args.viewport = argv[++i];
162
227
  else if (a === '--input') args.input = argv[++i];
163
228
  else if (a === '--output') args.output = argv[++i];
164
229
  else if (a === '--run') args.runId = argv[++i];
@@ -186,17 +251,77 @@ function parseArgs(argv) {
186
251
  else if (a === '--webhook') args.webhook = argv[++i];
187
252
  else if (a === '--min-severity') args.minSeverity = argv[++i];
188
253
  else if (a === '--monitor') args.monitor = true;
254
+ else if (a === '--mcp') args.mcp = true;
189
255
  else if (a === '--target-id') args.targetId = argv[++i];
190
256
  else if (a === '--reviewer') args.reviewer = argv[++i];
191
257
  else if (a === '--entity') args.entity = argv[++i];
258
+ else if (a === '--urls') args.urls = argv[++i];
259
+ else if (a === '--sitemap') args.sitemap = argv[++i];
260
+ else if (a === '--host') args.host = argv[++i];
261
+ else if (a === '--key') args.key = argv[++i];
262
+ else if (a === '--key-location') args.keyLocation = argv[++i];
263
+ else if (a === '--skip-key-verify') args.skipKeyVerify = true;
264
+ else if (a === '--allow-private-for-test') args.allowPrivateForTest = true;
265
+ else if (a === '--server') args.serverId = argv[++i];
266
+ else if (a === '--tool') args.toolName = argv[++i];
267
+ else if (a === '--transport') args.transportType = argv[++i];
268
+ else if (a === '--server-script') args.serverScript = argv[++i];
269
+ else if (a === '--tool-args' || a === '--args') args.toolArgs = argv[++i];
270
+ else if (a === '--site-url') args.siteUrl = argv[++i];
271
+ else if (a === '--until') args.until = argv[++i];
272
+ else if (a === '--reason') args.reason = argv[++i];
273
+ else if (a === '--id') args.id = argv[++i];
274
+ else if (a === '--evidence') args.evidence = argv[++i];
275
+ else if (a === '--note') args.note = argv[++i];
276
+ else if (a === '--expired') args.expiredOnly = true;
277
+ else if (a === '--expiring-soon') args.expiringSoonDays = Number(argv[++i]);
278
+ else if (a === '--finding') args.finding = argv[++i];
279
+ else if (a === '--component') args.component = argv[++i];
280
+ else if (a === '--format') args.format = argv[++i];
281
+ else if (a === '--strict') args.strict = true;
282
+ else if (a === '--export') args.export = argv[++i];
283
+ else if (a === '--apply') args.apply = true;
284
+ else if (a === '--preview-file') args['preview-file'] = argv[++i];
285
+ else if (a === '--observed-control') args['observed-control'] = argv[++i];
286
+ else if (a === '--observed-variant') args['observed-variant'] = argv[++i];
287
+ else if (a === '--days-running') args['days-running'] = argv[++i];
288
+ else if (a === '--baseline-rate') args['baseline-rate'] = argv[++i];
289
+ else if (a === '--p-value') args['p-value'] = argv[++i];
290
+ else if (a === '--revenue-claimed') args['revenue-claimed'] = true;
192
291
  else args._.push(a);
193
292
  }
194
293
  return args;
195
294
  }
196
295
 
197
- function out(args, human, data) {
198
- if (args.json) console.log(JSON.stringify(data, null, 2));
199
- else console.log(human);
296
+ const TOOL_VERSION_CLI = JSON.parse(
297
+ fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
298
+ ).version;
299
+
300
+ // Set by main(); read by out() for the JSON envelope. Kept off `args` because
301
+ // several commands spread parsed args directly into option objects.
302
+ let CURRENT_COMMAND = null;
303
+
304
+ /**
305
+ * Stable machine-readable output contract (citable_output_schema 1.0):
306
+ * every command's --json payload is wrapped in this envelope so CI systems
307
+ * and agencies can consume results without parsing terminal text. The
308
+ * envelope version changes only on breaking envelope changes; command
309
+ * payloads evolve backward-compatibly inside `result`.
310
+ */
311
+ const OUTPUT_SCHEMA_VERSION = '1.0';
312
+
313
+ function out(args, human, data, command = null) {
314
+ if (args.json) {
315
+ console.log(JSON.stringify({
316
+ citable_output_schema: OUTPUT_SCHEMA_VERSION,
317
+ tool_version: TOOL_VERSION_CLI,
318
+ command: command ?? CURRENT_COMMAND,
319
+ generated_at: new Date().toISOString(),
320
+ result: data,
321
+ }, null, 2));
322
+ } else {
323
+ console.log(human);
324
+ }
200
325
  }
201
326
 
202
327
  export async function main(argv = process.argv.slice(2), options = {}) {
@@ -207,6 +332,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
207
332
  }
208
333
  if (isInstallerCommand(cmd)) return runInstallerCommand(cmd, argv.slice(1), options);
209
334
  const args = parseArgs(argv.slice(1));
335
+ CURRENT_COMMAND = cmd;
210
336
  const root = options.cwd ?? process.cwd();
211
337
 
212
338
  try {
@@ -234,8 +360,20 @@ export async function main(argv = process.argv.slice(2), options = {}) {
234
360
  break;
235
361
  }
236
362
  case 'audit': {
363
+ if (args._[0] === 'edge') {
364
+ const file = args._[1];
365
+ if (!file || !args.format) throw new Error('usage: citable audit edge <file> --format <cloudflare-worker|cloudflare-cro|vercel-middleware|shopify-snippet|html>');
366
+ const content = fs.readFileSync(path.resolve(root, file), 'utf8');
367
+ const r = auditEdgeCode(content, { format: args.format, name: file });
368
+ const txt = `audit edge ${file} (${r.format}, ${r.byte_size} bytes)
369
+ Findings: ${r.finding_count} (high:${r.high} medium:${r.medium} advisory:${r.advisory})
370
+ ${r.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}\n Fix: ${f.remediation}`).join('\n') || ' no risk patterns found'}
371
+ Methodology: ${r.methodology}`;
372
+ out(args, txt, r);
373
+ break;
374
+ }
237
375
  const scope = args._[0];
238
- const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate });
376
+ const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
239
377
  out(args, `Audit ${r.runId}: ${r.summary.total} finding(s) [${Object.entries(r.summary.by_severity).map(([k, v]) => `${k}:${v}`).join(' ')}]\nEvidence package: ${r.dir}\nReport: ${path.join(r.dir, 'report.md')}\nStatus: ${r.manifest.status}${r.manifest.incomplete_checks.length ? `\nIncomplete: ${r.manifest.incomplete_checks.join('; ')}` : ''}`, { runId: r.runId, dir: r.dir, summary: r.summary, status: r.manifest.status });
240
378
  break;
241
379
  }
@@ -247,11 +385,338 @@ export async function main(argv = process.argv.slice(2), options = {}) {
247
385
  break;
248
386
  }
249
387
  case 'inspect': {
250
- if (!args._[0]) throw new Error('usage: citable inspect <page> --target <dir|url>');
388
+ if (!args._[0]) throw new Error('usage: citable inspect <page> --target <dir|url> OR citable inspect serp <page> OR citable inspect cro <page> OR citable inspect aeo <page> OR citable inspect geo <page>');
389
+ if (args._[0] === 'serp') {
390
+ if (!args._[1]) throw new Error('usage: citable inspect serp <page> --target <dir|url>');
391
+ const r = await inspectSerp(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
392
+ const richList = Object.entries(r.richSnippets).map(([k, v]) => ` ${k}: ${v.status}${v.issues?.length ? ` (${v.issues.join('; ')})` : ''}`).join('\n');
393
+ const txt = `Inspect SERP ${r.url}\n${r.visualCard}
394
+ Desktop Preview (max ~600px):
395
+ Breadcrumb: ${r.desktop.breadcrumb}
396
+ Title: ${r.desktop.title} (${r.desktop.titlePixelWidth}px${r.desktop.isTitleTruncated ? ' [TRUNCATED]' : ''})
397
+ Snippet: ${r.desktop.snippet}${r.desktop.isSnippetTruncated ? ' [TRUNCATED]' : ''}
398
+ Mobile Preview:
399
+ Breadcrumb: ${r.mobile.breadcrumb}
400
+ Title: ${r.mobile.title}${r.mobile.isTitleTruncated ? ' [TRUNCATED]' : ''}
401
+ Snippet: ${r.mobile.snippet}${r.mobile.isSnippetTruncated ? ' [TRUNCATED]' : ''}
402
+ Rich Snippet Eligibility:
403
+ ${richList}
404
+ Schema Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
405
+ out(args, txt, r);
406
+ break;
407
+ }
408
+ if (args._[0] === 'cro') {
409
+ if (!args._[1]) throw new Error('usage: citable inspect cro <page> --target <dir|url>');
410
+ const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
411
+ const txt = `Inspect CRO ${r.url}
412
+ Status: ${r.conversion_status}
413
+ Friction Surface Area: ${r.friction_surface_area}
414
+ Declared Conversion Action: ${r.declared_conversion_action || 'none'}
415
+ Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
416
+ Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
417
+ Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
418
+ Primary CTA Conspicuity: ${r.saliency?.primary_cta_conspicuity_index ?? 'N/A'} (${r.saliency?.pci_assessment ?? 'unassessed'}) — modeled heuristic index, not observed attention
419
+ Keystroke Effort: ${r.keystroke_effort?.autofill_coverage_pct}% autofill coverage (${r.keystroke_effort?.keystroke_reduction_pct}% modeled mobile friction reduction)
420
+ Payment Wallet Readiness: ${r.express_checkout_readiness?.payment_wallet_readiness?.supported ? 'SUPPORTED' : 'NOT DETECTED (CRO-021)'}
421
+ Authentication Readiness (passkeys/WebAuthn): ${r.express_checkout_readiness?.authentication_readiness?.supported ? 'DETECTED' : 'NOT DETECTED'} (informational index; authentication, not payment)
422
+ CTAs (${r.ctas.length}):
423
+ ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
424
+ Forms (${r.forms.length}):
425
+ ${r.forms.map((f, i) => ` Form ${i + 1}: ${f.fieldCount} fields (autocomplete: ${f.inputsWithAutocomplete}/${f.fieldCount}, labels: ${f.inputsWithLabel}/${f.fieldCount}), submit: ${f.hasSubmit ? 'yes' : 'NO'} → ${f.action || 'default'}`).join('\n') || ' none'}
426
+ Trust Signals: ${r.trustBadges.join(', ') || 'none'}
427
+ CRO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
428
+ out(args, txt, r);
429
+ break;
430
+ }
431
+ if (args._[0] === 'saliency') {
432
+ if (!args._[1]) throw new Error('usage: citable inspect saliency <page> --target <dir|url>');
433
+ const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
434
+ const sal = r.saliency;
435
+ const gazeLines = (sal?.predicted_gaze_path || []).map((g) => ` ${g.order}. [${g.type.toUpperCase()}] "${g.label}" (salience: ${Math.round(g.salience_score * 100)}%)`).join('\n');
436
+ const txt = `Inspect Saliency ${r.url}
437
+ Primary CTA Conspicuity Index: ${sal?.primary_cta_conspicuity_index ?? 'N/A'} [${sal?.pci_assessment ?? 'unassessed'}]
438
+ Visual Clutter Risk: ${sal?.visual_clutter ?? 'clean'} (${sal?.competing_hero_elements_count ?? 0} high-salience hero elements)
439
+ Predicted Gaze Path (First 3 Fixations):
440
+ ${gazeLines || ' none predicted'}
441
+ SVG Attention Heatmap: generated (${sal?.elements_evaluated ?? 0} elements mapped)`;
442
+ out(args, txt, { url: r.url, saliency: sal });
443
+ break;
444
+ }
445
+ if (args._[0] === 'aeo') {
446
+ if (!args._[1]) throw new Error('usage: citable inspect aeo <page> --target <dir|url>');
447
+ const r = await inspectAeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
448
+ const txt = `Inspect AEO ${r.url}
449
+ Extractability Status: ${r.extractability_status}
450
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
451
+ Headings (${r.headings.total} total, ${r.headings.questions.length} questions):
452
+ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') || ' none'}
453
+ Answer Structure:
454
+ Definitional Copula: ${r.answer_structure.has_copular_definition ? 'YES' : 'NO'}
455
+ Executive Summary / Key Takeaways: ${r.answer_structure.has_executive_summary ? 'YES' : 'NO'}
456
+ Tables: ${r.answer_structure.tables_count}; Ordered Lists: ${r.answer_structure.ordered_lists_count}
457
+ AEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
458
+ out(args, txt, r);
459
+ break;
460
+ }
461
+ if (args._[0] === 'geo') {
462
+ if (!args._[1]) throw new Error('usage: citable inspect geo <page> --target <dir|url>');
463
+ const r = await inspectGeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
464
+ const txt = `Inspect GEO ${r.url}
465
+ Retrieval Posture: ${r.retrieval_posture}
466
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
467
+ RAG Metrics:
468
+ Estimated 300w Chunks: ${r.rag_metrics.estimated_chunks_300w}
469
+ Headings: ${r.rag_metrics.total_headings}
470
+ Average Words/Chunk: ${r.rag_metrics.average_words_per_chunk}
471
+ GEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
472
+ out(args, txt, r);
473
+ break;
474
+ }
251
475
  const r = await inspect(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
252
476
  out(args, `Inspect ${r.url}\n title: ${r.title}\n status: ${r.status}; canonical: ${r.canonicals.join(', ') || 'none'}; robots: ${r.robotsDirectives.join(',') || 'default'}\n intent: ${r.primary_intent ?? 'undeclared'}; conversion: ${r.conversion_action ?? 'undeclared'}\n entities: ${r.entities.map((e) => e.name).join(', ') || 'none'}\n claims: ${r.claims.length}; findings: ${r.findings.length}\n ambiguity:\n${r.unresolved_ambiguity.map((a) => ` - ${a}`).join('\n') || ' none'}`, r);
253
477
  break;
254
478
  }
479
+ case 'inspect-serp': {
480
+ if (!args._[0]) throw new Error('usage: citable inspect-serp <page> --target <dir|url>');
481
+ const r = await inspectSerp(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
482
+ const richList = Object.entries(r.richSnippets).map(([k, v]) => ` ${k}: ${v.status}${v.issues?.length ? ` (${v.issues.join('; ')})` : ''}`).join('\n');
483
+ const txt = `Inspect SERP ${r.url}\n${r.visualCard}
484
+ Desktop Preview (max ~600px):
485
+ Breadcrumb: ${r.desktop.breadcrumb}
486
+ Title: ${r.desktop.title} (${r.desktop.titlePixelWidth}px${r.desktop.isTitleTruncated ? ' [TRUNCATED]' : ''})
487
+ Snippet: ${r.desktop.snippet}${r.desktop.isSnippetTruncated ? ' [TRUNCATED]' : ''}
488
+ Mobile Preview:
489
+ Breadcrumb: ${r.mobile.breadcrumb}
490
+ Title: ${r.mobile.title}${r.mobile.isTitleTruncated ? ' [TRUNCATED]' : ''}
491
+ Snippet: ${r.mobile.snippet}${r.mobile.isSnippetTruncated ? ' [TRUNCATED]' : ''}
492
+ Rich Snippet Eligibility:
493
+ ${richList}
494
+ Schema Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
495
+ out(args, txt, r);
496
+ break;
497
+ }
498
+ case 'inspect-cro': {
499
+ if (!args._[0]) throw new Error('usage: citable inspect-cro <page> --target <dir|url>');
500
+ const r = await inspectCro(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
501
+ const txt = `Inspect CRO ${r.url}
502
+ Status: ${r.conversion_status}
503
+ Declared Conversion Action: ${r.declared_conversion_action || 'none'}
504
+ Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
505
+ Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
506
+ Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
507
+ CTAs (${r.ctas.length}):
508
+ ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
509
+ Forms (${r.forms.length}):
510
+ ${r.forms.map((f, i) => ` Form ${i + 1}: ${f.fieldCount} fields (autocomplete: ${f.inputsWithAutocomplete}/${f.fieldCount}, labels: ${f.inputsWithLabel}/${f.fieldCount}), submit: ${f.hasSubmit ? 'yes' : 'NO'} → ${f.action || 'default'}`).join('\n') || ' none'}
511
+ Trust Signals: ${r.trustBadges.join(', ') || 'none'}
512
+ CRO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
513
+ out(args, txt, r);
514
+ break;
515
+ }
516
+ case 'inspect-aeo': {
517
+ if (!args._[0]) throw new Error('usage: citable inspect-aeo <page> --target <dir|url>');
518
+ const r = await inspectAeo(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
519
+ const txt = `Inspect AEO ${r.url}
520
+ Extractability Status: ${r.extractability_status}
521
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
522
+ Headings (${r.headings.total} total, ${r.headings.questions.length} questions):
523
+ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') || ' none'}
524
+ Answer Structure:
525
+ Definitional Copula: ${r.answer_structure.has_copular_definition ? 'YES' : 'NO'}
526
+ Executive Summary / Key Takeaways: ${r.answer_structure.has_executive_summary ? 'YES' : 'NO'}
527
+ Tables: ${r.answer_structure.tables_count}; Ordered Lists: ${r.answer_structure.ordered_lists_count}
528
+ AEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
529
+ out(args, txt, r);
530
+ break;
531
+ }
532
+ case 'inspect-geo': {
533
+ if (!args._[0]) throw new Error('usage: citable inspect-geo <page> --target <dir|url>');
534
+ const r = await inspectGeo(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
535
+ const txt = `Inspect GEO ${r.url}
536
+ Retrieval Posture: ${r.retrieval_posture}
537
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
538
+ RAG Metrics:
539
+ Estimated 300w Chunks: ${r.rag_metrics.estimated_chunks_300w}
540
+ Headings: ${r.rag_metrics.total_headings}
541
+ Average Words/Chunk: ${r.rag_metrics.average_words_per_chunk}
542
+ GEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
543
+ out(args, txt, r);
544
+ break;
545
+ }
546
+ case 'preview': {
547
+ const sub = args._[0];
548
+ if (sub !== 'cro') throw new Error('usage: citable preview cro <page> [--target <dir|url>] [--export <path>]');
549
+ const page = args._[1] || 'home';
550
+ const r = await previewCroCommand(root, page, { target: args.target, export: args.export });
551
+ const txt = `Preview CRO ${r.url}
552
+ Friction Surface Area (Before): ${r.fsa_before}
553
+ Friction Surface Area (Modeled Target): ${r.fsa_after} (modeled index, not a measured outcome)
554
+ Resolved Findings: ${r.findings_count}
555
+ HTML Preview: ${r.saved_file || 'rendered in memory (use --export <file.html> to save)'}`;
556
+ out(args, txt, r);
557
+ break;
558
+ }
559
+ case 'remediate': {
560
+ const r = await remediateCommand(root, {
561
+ finding: args.finding || args._[0],
562
+ component: args.component,
563
+ target: args.target,
564
+ format: args.format || 'react',
565
+ write: args.write,
566
+ });
567
+ if (r.target) {
568
+ const parts = [`Remediate ${r.finding_id} → ${r.recommended_component}`, ` Rationale: ${r.rationale}`, ` Scaffold Command: ${r.scaffold_command}`];
569
+ parts.push(` Target File: ${r.target}`);
570
+ parts.push(` Framework: ${r.framework?.framework || 'unknown'} (detection confidence ${r.framework?.confidence ?? 0})`);
571
+ parts.push(` Patch: ${r.changed ? `${r.match_count} match(es)` : 'no match (already remediated or not applicable)'}, idempotent: ${r.idempotent}, class: ${r.patcher_class}`);
572
+ if (r.validation) {
573
+ for (const c of r.validation.checks) parts.push(` Check ${c.check_id}: ${c.passed ? 'PASS' : 'FAIL'} — ${c.detail}`);
574
+ if (r.validation.methodology) parts.push(` Methodology: ${r.validation.methodology}`);
575
+ }
576
+ if (r.confidence) {
577
+ parts.push(` Confidence: ${r.confidence.score} (write threshold ${r.write_policy?.min_confidence ?? 0.7})`);
578
+ for (const f of r.confidence.factors) parts.push(` - ${f.factor}: ${f.weight} (${f.detail})`);
579
+ }
580
+ parts.push(r.written
581
+ ? ` Written. Rollback snapshot: ${r.rollback?.snapshot_file}`
582
+ : r.write_refused
583
+ ? ` Write REFUSED (fail closed): ${r.refusal_reason}`
584
+ : ` Dry run — unified diff below. Use --write to apply.`);
585
+ if (r.diff) parts.push('Unified Diff:', r.diff.trimEnd());
586
+ parts.push('', 'Code Snippet (recommended component):', '--------------------------------------------------', r.code, '--------------------------------------------------');
587
+ out(args, parts.join('\n'), r);
588
+ break;
589
+ }
590
+ const txt = r.recommended_component
591
+ ? `Remediate ${r.finding_id} → ${r.recommended_component}
592
+ Rationale: ${r.rationale}
593
+ Scaffold Command: ${r.scaffold_command}
594
+ Code Snippet (${r.format}):
595
+ --------------------------------------------------
596
+ ${r.code}
597
+ --------------------------------------------------`
598
+ : r.component
599
+ ? `Remediate Component: ${r.component}
600
+ Scaffold Command: ${r.scaffold_command}
601
+ Code Snippet:
602
+ --------------------------------------------------
603
+ ${r.code}
604
+ --------------------------------------------------`
605
+ : `Available Components:
606
+ ${(r.available_components || []).map((c) => ` - ${c.name} (${c.id}): ${c.description}`).join('\n')}
607
+ Supported Findings: ${(r.supported_findings || []).join(', ')}`;
608
+ out(args, txt, r);
609
+ break;
610
+ }
611
+ case 'test-funnel':
612
+ case 'test': {
613
+ if (cmd === 'test' && args._[0] === 'visual') {
614
+ const preview = args['preview-file'] ? fs.readFileSync(path.resolve(root, args['preview-file']), 'utf8') : null;
615
+ const r = await runVisualRegression(root, { previewHtml: preview, output: args.output });
616
+ const txt = `test visual
617
+ Contract checks: ${r.contract.ok ? 'ALL PASS' : 'FAIL'} (${r.contract.checks.filter((c) => c.passed).length}/${r.contract.checks.length})
618
+ ${r.contract.checks.map((c) => ` ${c.passed ? '✔' : '✖'} ${c.check_id}: ${c.detail}`).join('\n')}
619
+ Matrix cells: ${r.matrix_cells.length}
620
+ Screenshots: ${r.screenshots.status}${r.screenshots.required_input ? ` (required_input: ${r.screenshots.required_input})` : ` (${r.screenshots.captured.length} captured)`}`;
621
+ out(args, txt, r);
622
+ break;
623
+ }
624
+ if (cmd === 'test-funnel' || args._[0] === 'funnel') {
625
+ const funnelId = cmd === 'test-funnel' ? args._[0] : args._[1];
626
+ const r = await testFunnel(root, funnelId, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
627
+ const txt = `Test Funnel ${r.funnel_id} (${r.name})
628
+ Status: ${r.status}
629
+ Primary Goal: ${r.primary_goal}
630
+ Steps Verified: ${r.steps_verified}/${r.total_steps}
631
+ Steps:
632
+ ${r.steps.map((s) => ` [${s.role.toUpperCase()}] ${s.name} (${s.url_pattern}) → HTTP ${s.status || 'not found'}${s.next_step_linked ? ' (linked)' : ''}`).join('\n')}
633
+ Issues (${r.issues.length}):
634
+ ${r.issues.map((iss) => ` - ${iss}`).join('\n') || ' none'}
635
+ Funnel Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
636
+ out(args, txt, r);
637
+ break;
638
+ }
639
+ if (args._[0] === 'edge') {
640
+ const r = await testEdgeRules(root, { provider: args.provider, baseUrl: args.baseUrl });
641
+ out(args, `Test Edge (${r.provider}): ${r.status.toUpperCase()} (${r.passed_checks}/${r.total_checks} checks passed)`, r);
642
+ break;
643
+ }
644
+ throw new Error('usage: citable test funnel [funnel_id] --target <dir|url> OR citable test edge [--provider cloudflare]');
645
+ }
646
+ case 'fleet': {
647
+ const sub = args._[0] ?? 'summary';
648
+ if (sub === 'summary') {
649
+ const r = await fleetSummary(root);
650
+ out(args, `Fleet Summary (${r.total_properties} properties, ${r.active_properties} active)\n` + r.properties.map((p) => ` [${p.environment.toUpperCase()}] ${p.name} (${p.primary_domain})`).join('\n'), r);
651
+ } else if (sub === 'audit') {
652
+ const r = await fleetAudit(root, { propertyId: args.property, environment: args.env, scope: args.scope });
653
+ out(args, `Fleet Audit: ${r.fleet_size} properties audited (health: ${r.fleet_health.toUpperCase()})\nTotal findings: ${r.total_findings} (critical: ${r.total_critical}, high: ${r.total_high})`, r);
654
+ } else {
655
+ throw new Error('usage: citable fleet [summary|audit] [--property <id>] [--env <environment>]');
656
+ }
657
+ break;
658
+ }
659
+ case 'probe': {
660
+ const query = args._[0];
661
+ if (!query) throw new Error('usage: citable probe <query|prompt_id> [--engine <perplexity|openai|gemini>] [--domain <domain>]');
662
+ const r = await probeEngine(root, query, { engine: args.engine, targetDomain: args.domain, output: args.output });
663
+ out(args, `Probe [${r.data.provider}]: ${r.data.property_cited ? 'CITING' : 'NOT CITING'} ${r.data.target_domain}\n${r.data.raw_response}`, r);
664
+ break;
665
+ }
666
+ case 'lint': {
667
+ const sub = args._[0];
668
+ if (sub === 'components') {
669
+ const dir = args._[1] || 'src/components';
670
+ const r = await lintComponents(dir);
671
+ out(args, `Lint Components: ${r.total_files_scanned} files scanned (${r.status.toUpperCase()})\nFindings: ${r.total_findings}\n` + r.findings.map((f) => ` [${f.rule_id}] ${f.file}:${f.line} - ${f.name}`).join('\n'), r);
672
+ break;
673
+ }
674
+ throw new Error('usage: citable lint components [dir]');
675
+ }
676
+ case 'export': {
677
+ const sub = args._[0];
678
+ if (sub === 'edge') {
679
+ const format = args.format || 'cloudflare-redirects';
680
+ const r = await exportEdgeRules(root, { format, output: args.output });
681
+ const sec = r.security;
682
+ out(args, `Exported ${r.rules_count} edge rule(s) in format "${r.format}"${r.output_path ? ` to ${r.output_path}` : ''}\nSecurity audit: ${sec.high} high, ${sec.medium} medium, ${sec.advisory} advisory finding(s)\n${sec.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}`).join('\n') || ' no risk patterns found'}\n${r.content}`, r);
683
+ break;
684
+ }
685
+ throw new Error('usage: citable export edge [--format <cloudflare-redirects|cloudflare-waf|cloudflare-worker>]');
686
+ }
687
+ case 'attribute': {
688
+ const sub = args._[0];
689
+ if (sub === 'impact') {
690
+ const r = await attributeImpact(root, { beforeRunId: args.before, afterRunId: args.after, metricKey: args.metric, baselineValue: args.baseline ? Number(args.baseline) : undefined, observedValue: args.observed ? Number(args.observed) : undefined });
691
+ out(args, `Attribute Impact (${r.metric}):\n Treatment Lift: ${r.treatment.relative_change_percent}%\n Control Lift: ${r.control.relative_change_percent}%\n Diff-in-Diff Adjusted Lift: ${r.difference_in_differences_lift_percent}%\n Verdict: ${r.attribution_verdict}\n\nNotice: ${r.statistical_caveat}`, r);
692
+ break;
693
+ }
694
+ throw new Error('usage: citable attribute impact --before <run> --after <run>');
695
+ }
696
+ case 'plan-experiment':
697
+ case 'plan': {
698
+ if (cmd === 'plan-experiment' || args._[0] === 'experiment') {
699
+ const r = await planExperiment(root, args);
700
+ const txt = `Plan A/B Experiment
701
+ Baseline Conversion Rate: ${(r.baseline_conversion_rate * 100).toFixed(1)}%
702
+ Target Conversion Rate: ${(r.target_conversion_rate * 100).toFixed(2)}% (+${Math.round(r.minimum_detectable_effect_relative * 100)}% relative lift)
703
+ Statistical Power: ${Math.round(r.statistical_power * 100)}% (alpha = ${r.significance_level_alpha})
704
+ Sample Size Required: ${r.sample_size_per_variant.toLocaleString()} per variant (${r.total_sample_size.toLocaleString()} total)
705
+ Daily Visitors: ${r.daily_visitors.toLocaleString()}
706
+ Estimated Duration: ${r.estimated_duration_days} days
707
+ Risk Level: ${r.risk_level.toUpperCase()}
708
+ Recommendations:
709
+ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
710
+ out(args, txt, r);
711
+ break;
712
+ }
713
+ if (cmd === 'plan' && args._[0] === 'audit') {
714
+ const p = await planAudit(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
715
+ out(args, formatPlan(p), p);
716
+ break;
717
+ }
718
+ throw new Error('usage: citable plan experiment --baseline-rate <0.02> --mde <0.10> --daily-visitors <500> OR citable plan-audit');
719
+ }
255
720
  case 'map-claims': {
256
721
  const r = await mapClaims(root, { target: args.target, baseUrl: args.baseUrl, write: args.write, refDate: args.refDate });
257
722
  out(args, `map-claims: ${r.candidates.length} candidate claim(s)${args.write ? `, ${r.written} written to registry as candidate/review_required` : ' (dry run — use --write to save)'}\n` + r.candidates.map((c) => ` [${c.claim_type}${c.review_required ? ', REVIEW REQUIRED' : ''}] ${c.claim.slice(0, 100)}\n at ${c.source_location}`).join('\n'), r);
@@ -306,12 +771,17 @@ export async function main(argv = process.argv.slice(2), options = {}) {
306
771
  runB: args._[1],
307
772
  webhookUrl: args.webhook,
308
773
  minSeverity: args.minSeverity,
774
+ allowPrivateForTest: args.allowPrivateForTest,
309
775
  });
310
776
  let msg = `monitor ${r.run_a} → ${r.run_b}: ${r.summary.alerts} alert(s), ${r.summary.critical_or_high} critical/high\nReport: ${path.join(r.dir, 'latest.json')}`;
311
777
  if (r.delivery) {
312
778
  if (r.delivery.skipped) msg += `\nDelivery: skipped (${r.delivery.reason})`;
313
- else if (r.delivery.success) msg += `\nDelivery: webhook dispatched to ${r.delivery.target_url} (HTTP ${r.delivery.status_code})`;
314
- else msg += `\nDelivery: webhook FAILED (${r.delivery.error})`;
779
+ else if (r.delivery.success) {
780
+ msg += `\nDelivery: webhook dispatched to ${r.delivery.target_url} (HTTP ${r.delivery.status_code})`;
781
+ if (r.delivery.signature) msg += ` (signed: ${r.delivery.signature.slice(0, 16)}...)`;
782
+ } else {
783
+ msg += `\nDelivery: webhook FAILED (${r.delivery.error})`;
784
+ }
315
785
  }
316
786
  out(args, msg, r);
317
787
  if (r.summary.critical_or_high > 0) process.exitCode = 1;
@@ -325,8 +795,14 @@ export async function main(argv = process.argv.slice(2), options = {}) {
325
795
  } else if (sub === 'share-of-voice' || sub === 'citations' || sub === 'share') {
326
796
  const r = reportShareOfVoice(root, { since: args.since, last: args.last ? Number(args.last) : undefined });
327
797
  out(args, `report share-of-voice: ${r.included} citation run(s) included, ${r.competitors_evaluated} competitor(s) evaluated\nMarkdown: ${r.path_md}\nHTML: ${r.path_html}`, r);
798
+ } else if (sub === 'consensus') {
799
+ const r = reportConsensus(root, { runId: args.runId, since: args.since, last: args.last ? Number(args.last) : undefined });
800
+ out(args, `report consensus: ${r.urls_evaluated} URL(s) evaluated (${r.canonical_consensus_count} consensus, ${r.conflicts_count} conflict(s))\nMarkdown: ${r.path_md}\nHTML: ${r.path_html}`, r);
801
+ } else if (sub === 'export') {
802
+ const r = await exportExecutiveReport(root, args.runId, { format: args.format, clientName: args.client, output: args.output });
803
+ out(args, `Executive Report Exported (${r.format}) for "${r.client_name}"${r.output_path ? ` to ${r.output_path}` : ''}\n\n${r.content}`, r);
328
804
  } else {
329
- throw new Error('usage: citable report <dashboard|share-of-voice> [--last <n>] [--since <run-id>]');
805
+ throw new Error('usage: citable report <dashboard|share-of-voice|consensus|export> [--last <n>] [--since <run-id>] [--run <run-id>] [--format <html-brief|markdown-deck>]');
330
806
  }
331
807
  break;
332
808
  }
@@ -363,7 +839,41 @@ export async function main(argv = process.argv.slice(2), options = {}) {
363
839
  } else if (mode === 'disconnect') {
364
840
  const r = disconnectConnection(root, args);
365
841
  out(args, `connect disconnect ${r.connection_id}: ${r.disconnected ? 'removed' : 'dry run; use --write to remove'}`, r);
366
- } else throw new Error('usage: citable connect <status|configure|discover|validate|sync|read|apply|disconnect> [options]');
842
+ } else if (mode === 'indexnow') {
843
+ const r = await submitIndexNow(root, args);
844
+ const batchInfo = r.batch_count > 1 ? ` in ${r.batch_count} batch(es)` : '';
845
+ const actionDesc = r.dry_run ? '(dry run; use --write to submit)' : (r.success ? `submitted (status: ${r.batches[0]?.status_code ?? 200})` : `FAILED (${r.error})`);
846
+ out(args, `connect indexnow: ${r.url_count} URL(s)${batchInfo} for ${r.host} ${actionDesc}\nReceipt: ${r.receipt_file}`, r);
847
+ if (!r.success) process.exitCode = 1;
848
+ } else if (mode === 'mcp') {
849
+ const toolArgs = args.toolArgs ? JSON.parse(args.toolArgs) : (args.target ? { url: args.target } : (args.urls ? { url: args.urls } : {}));
850
+ if (args.target && !toolArgs.url) toolArgs.url = args.target;
851
+ if (args.siteUrl && !toolArgs.siteUrl) toolArgs.siteUrl = args.siteUrl;
852
+ else if (args.target && !toolArgs.siteUrl && args.serverId === 'gsc-mcp') {
853
+ try { toolArgs.siteUrl = new URL(args.target).origin; } catch { toolArgs.siteUrl = args.target; }
854
+ }
855
+ if (args.startDate && !toolArgs.startDate) toolArgs.startDate = args.startDate;
856
+ if (args.endDate && !toolArgs.endDate) toolArgs.endDate = args.endDate;
857
+ const r = await collectMcpEvidence(root, {
858
+ ...args,
859
+ args: toolArgs,
860
+ });
861
+ out(args, `connect mcp: tool "${r.transportEnvelope.tool.name}" on server "${r.transportEnvelope.server.server_id}" executed via ${r.transportEnvelope.transport.type} (status: ${r.transportEnvelope.status})\nRun: ${r.runId}\nPackage: ${r.dir}`, r);
862
+ if (r.transportEnvelope.status !== 'success') process.exitCode = 1;
863
+ } else throw new Error('usage: citable connect <status|configure|discover|validate|sync|read|apply|disconnect|indexnow|mcp> [options]');
864
+ break;
865
+ }
866
+ case 'generate': {
867
+ const sub = args._[0];
868
+ if (sub === 'llms-txt' || sub === 'llms.txt') {
869
+ const r = generateLlmsTxt(root, { output: args.output, siteUrl: args.siteUrl, write: args.write });
870
+ out(args, `generate llms-txt: ${r.written ? 'written' : 'dry run (use --write to save)'}\nOutput: ${r.outputDir}\nPages indexed: ${r.pageCount}\nClaims declared: ${r.claimCount}`, r);
871
+ } else throw new Error('usage: citable generate <llms-txt> [--output <path>] [--site-url <url>] [--write]');
872
+ break;
873
+ }
874
+ case 'generate-llms-txt': {
875
+ const r = generateLlmsTxt(root, { output: args.output, siteUrl: args.siteUrl, write: args.write });
876
+ out(args, `generate llms-txt: ${r.written ? 'written' : 'dry run (use --write to save)'}\nOutput: ${r.outputDir}\nPages indexed: ${r.pageCount}\nClaims declared: ${r.claimCount}`, r);
367
877
  break;
368
878
  }
369
879
  case 'objectives': {
@@ -397,6 +907,40 @@ export async function main(argv = process.argv.slice(2), options = {}) {
397
907
  } else throw new Error('usage: citable governance <validate|evaluate [run-id]> [--ref-date YYYY-MM-DD]');
398
908
  break;
399
909
  }
910
+ case 'exceptions': {
911
+ const mode = args._[0];
912
+ if (mode === 'list') {
913
+ const r = listExceptions(root, {
914
+ expiredOnly: args.expiredOnly,
915
+ expiringSoonDays: args.expiringSoonDays,
916
+ refDate: args.refDate,
917
+ });
918
+ const rows = r.exceptions.map((e) =>
919
+ ` ${e.exception_id} [${e.computed_status}]: expires ${e.expires_at.slice(0, 10)} (${e.days_remaining}d remaining, renewed ${e.renewal_count}/${e.renewal_limit}) - ${e.reason}`
920
+ );
921
+ out(args, `exceptions list: ${r.exceptions.length} exception(s) (total: ${r.summary.total}, active: ${r.summary.active}, expiring soon: ${r.summary.expiring_soon}, expired: ${r.summary.expired})\n` + (rows.length ? rows.join('\n') : ' (no matching exceptions)'), r);
922
+ } else if (mode === 'renew') {
923
+ const r = renewException(root, {
924
+ id: args.id || args._[1],
925
+ until: args.until,
926
+ reviewer: args.reviewer,
927
+ evidence: args.evidence,
928
+ note: args.note || args.reason,
929
+ write: args.write,
930
+ refDate: args.refDate,
931
+ });
932
+ out(args, `exceptions renew: ${r.exception_id} extended to ${r.expires_at} (renewal count: ${r.renewal_count}/${r.renewal_limit}) ${r.written ? 'written' : '(dry run; use --write to save)'}`, r);
933
+ } else if (mode === 'invalidate') {
934
+ const r = invalidateException(root, {
935
+ id: args.id || args._[1],
936
+ reason: args.reason,
937
+ reviewer: args.reviewer,
938
+ write: args.write,
939
+ });
940
+ out(args, `exceptions invalidate: ${r.exception_id} marked revoked ${r.written ? 'written' : '(dry run; use --write to save)'}`, r);
941
+ } else throw new Error('usage: citable exceptions <list|renew|invalidate> [options]');
942
+ break;
943
+ }
400
944
  case 'reviews': {
401
945
  const mode=args._[0]; let r;
402
946
  if(mode==='queue') r=queueReviews(root,{runId:args._[1],policyId:args._[2],write:args.write});
@@ -456,7 +1000,14 @@ export async function main(argv = process.argv.slice(2), options = {}) {
456
1000
  } else if (mode === 'compare-receipts') {
457
1001
  const r = readAndCompareAcceptanceReceipts(args._[1], args._[2]);
458
1002
  out(args, `corpus compare-receipts ${r.receipt_a} → ${r.receipt_b}\nComparable envelope: ${r.comparable}\nFingerprint equal: ${r.fingerprint_equal}\nPartial runs: ${r.partial_runs.length}`, r);
459
- } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts> [options]');
1003
+ } else if (mode === 'benchmark') {
1004
+ const r = await runGoldenBenchmark(root, { corpusDir: args.input, writeReport: args.output });
1005
+ const txt = `corpus benchmark v${r.corpus_version}: ${r.pages_evaluated} page(s) across ${r.sites_evaluated} site(s)
1006
+ Gate: ${r.gate.ok ? 'PASS' : 'FAIL'} (violations: ${r.gate.violation_count}, recall failures: ${r.gate.recall_failures.join(', ') || 'none'}, precision failures: ${r.gate.precision_failures.join(', ') || 'none'})
1007
+ ${r.per_detector.filter((d) => d.true_positives + d.false_negatives + d.false_positives > 0).map((d) => ` ${d.detector_id}: TP ${d.true_positives}, FP ${d.false_positives}, FN ${d.false_negatives}, P ${d.precision ?? 'n/a'}, R ${d.recall ?? 'n/a'}`).join('\n')}
1008
+ Limitations: ${r.limitations[0]}`;
1009
+ out(args, txt, r);
1010
+ } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts|benchmark> [options]');
460
1011
  break;
461
1012
  }
462
1013
  case 'artifacts': {
@@ -473,6 +1024,93 @@ export async function main(argv = process.argv.slice(2), options = {}) {
473
1024
  } else throw new Error('usage: citable artifacts <export <run-id> --output <directory>|verify --input <directory>|import --input <directory>>');
474
1025
  break;
475
1026
  }
1027
+ case 'verify': {
1028
+ const mode = args._[0];
1029
+ if (mode === 'page') {
1030
+ const r = await verifyPage(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
1031
+ const sev = Object.entries(r.by_severity).filter(([, n]) => n > 0).map(([k, n]) => `${k}:${n}`).join(' ') || 'none';
1032
+ const txt = `verify page ${r.page}
1033
+ Status: ${r.status}
1034
+ Findings: ${r.finding_count} (${sev})
1035
+ Detectors run: ${r.provenance.detectors_run.length}
1036
+ Definition: ${r.provenance.definition}`;
1037
+ out(args, txt, r);
1038
+ break;
1039
+ }
1040
+ if (mode !== 'remediation') throw new Error('usage: citable verify <remediation --run <run-id> --finding <detector-id> [|page <page>] [--target <dir|file>] [--apply]');
1041
+ const r = await verifyRemediation(root, {
1042
+ run: args.runId || args._[1],
1043
+ finding: args.finding,
1044
+ target: args.target,
1045
+ apply: args.apply,
1046
+ subject: args.subject,
1047
+ baseUrl: args['base-url'],
1048
+ refDate: args['ref-date'],
1049
+ });
1050
+ const subject = r.subject?.identifier || 'unknown subject';
1051
+ const patchLine = r.patch
1052
+ ? `\n Patch: applied=${r.patch.applied}${r.patch.refusal_reason ? ` (refused: ${r.patch.refusal_reason})` : ''}${r.patch.rollback_snapshot ? `\n Rollback snapshot: ${r.patch.rollback_snapshot}` : ''}`
1053
+ : '';
1054
+ const txt = `verify remediation ${r.detector_id} on ${subject}
1055
+ Verdict: ${r.status}${r.verdict.resolved ? ' (finding no longer reported)' : ''}
1056
+ Before finding(s): ${r.verdict.before_finding_ids.join(', ') || 'none'}
1057
+ After finding(s): ${r.verdict.after_finding_ids.join(', ') || 'none'}
1058
+ New critical/high regressions: ${r.verdict.new_regressions.length}${patchLine}
1059
+ Re-check target: ${r.provenance.recheck_target || 'n/a'}
1060
+ Resolution criterion: detector absence for the same subject; not an outcome guarantee
1061
+ Bundle: ${r.bundle_dir || 'not written'}`;
1062
+ out(args, txt, r);
1063
+ break;
1064
+ }
1065
+ case 'kit': {
1066
+ if (args._[0] !== 'export') throw new Error('usage: citable kit export --run <run-id> --finding <detector-id> [--target <file>] [--subject <id|url>] [--output <dir>]');
1067
+ const r = await exportImplementationKit(root, {
1068
+ run: args.runId,
1069
+ finding: args.finding,
1070
+ target: args.target,
1071
+ subject: args.subject,
1072
+ output: args.output,
1073
+ format: args.format,
1074
+ });
1075
+ const txt = `kit export ${r.detector_id}
1076
+ Kit directory: ${r.kit_dir}
1077
+ Files: ${r.files.length}
1078
+ Rendering evidence files: ${r.rendering_evidence_count}
1079
+ Patch diff included: ${r.has_patch_diff}`;
1080
+ out(args, txt, r);
1081
+ break;
1082
+ }
1083
+ case 'compatibility': {
1084
+ const r = await compatibilityCommand(root, args);
1085
+ const txt = `compatibility (citable ${r.tool_version}, Node ${r.node})
1086
+ ${r.checks.map((c) => ` [${c.severity.toUpperCase()}] ${c.check_id}: ${c.detail}`).join('\n')}
1087
+ Blockers: ${r.blocker_count}
1088
+ Note: ${r.note}`;
1089
+ out(args, txt, r);
1090
+ break;
1091
+ }
1092
+ case 'check': {
1093
+ if (args._[0] === 'experiment') {
1094
+ const r = await checkExperiment(root, {
1095
+ experimentId: args._[1],
1096
+ observedControl: args['observed-control'] !== undefined ? Number(args['observed-control']) : undefined,
1097
+ observedVariant: args['observed-variant'] !== undefined ? Number(args['observed-variant']) : undefined,
1098
+ daysRunning: args['days-running'] !== undefined ? Number(args['days-running']) : undefined,
1099
+ baselineRate: args['baseline-rate'],
1100
+ mde: args.mde,
1101
+ pValue: args['p-value'],
1102
+ revenueClaimed: args['revenue-claimed'],
1103
+ });
1104
+ const txt = `check experiment ${r.experiment_id}
1105
+ Lifecycle: ${r.lifecycle} — ${r.lifecycle_definition}
1106
+ Guardrails: ${r.guardrail_count} finding(s)
1107
+ ${r.findings.map((f) => ` [${f.guardrail_id}] (${f.severity}) ${f.summary}`).join('\n') || ' no guardrail findings'}
1108
+ Note: ${r.note}`;
1109
+ out(args, txt, r);
1110
+ break;
1111
+ }
1112
+ throw new Error('usage: citable check experiment <id> [--observed-control N --observed-variant N --days-running N --p-value X --revenue-claimed]');
1113
+ }
476
1114
  case 'self-upgrade': {
477
1115
  const output = await selfUpgradeCommand(argv.slice(1));
478
1116
  console.log(output);