@nebulacomponents/citable 1.15.0 → 1.16.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 (212) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +3 -3
  3. package/dist/universal/.agents/skills/citable/SKILL.md +5 -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 +14 -11
  10. package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
  11. package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
  12. package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
  13. package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  14. package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
  15. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  16. package/dist/universal/.claude/skills/citable/SKILL.md +5 -1
  17. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  18. package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
  19. package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
  20. package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
  21. package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
  22. package/dist/universal/.claude/skills/citable/manifest.json +14 -11
  23. package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
  24. package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
  25. package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
  26. package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  27. package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
  28. package/dist/universal/.cursor/skills/citable/SKILL.md +5 -1
  29. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  30. package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
  31. package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
  32. package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
  33. package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
  34. package/dist/universal/.cursor/skills/citable/manifest.json +14 -11
  35. package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
  36. package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
  38. package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  39. package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
  40. package/dist/universal/.gemini/skills/citable/SKILL.md +5 -1
  41. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  42. package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
  43. package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
  44. package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
  45. package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
  46. package/dist/universal/.gemini/skills/citable/manifest.json +14 -11
  47. package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
  48. package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
  49. package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  51. package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
  52. package/dist/universal/.github/skills/citable/SKILL.md +5 -1
  53. package/dist/universal/.github/skills/citable/VERSION +1 -1
  54. package/dist/universal/.github/skills/citable/commands/README.md +3 -1
  55. package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
  56. package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
  57. package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
  58. package/dist/universal/.github/skills/citable/manifest.json +14 -11
  59. package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
  60. package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
  61. package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
  62. package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  63. package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
  64. package/dist/universal/.kiro/skills/citable/SKILL.md +5 -1
  65. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  66. package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
  67. package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
  68. package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
  69. package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
  70. package/dist/universal/.kiro/skills/citable/manifest.json +14 -11
  71. package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
  72. package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
  73. package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
  74. package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  75. package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
  76. package/dist/universal/.opencode/skills/citable/SKILL.md +5 -1
  77. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  78. package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
  79. package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
  80. package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
  81. package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
  82. package/dist/universal/.opencode/skills/citable/manifest.json +14 -11
  83. package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
  84. package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
  85. package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
  86. package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  87. package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
  88. package/dist/universal/.pi/agent/skills/citable/SKILL.md +5 -1
  89. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  90. package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
  91. package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
  92. package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
  93. package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
  94. package/dist/universal/.pi/agent/skills/citable/manifest.json +14 -11
  95. package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
  96. package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
  97. package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
  98. package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  99. package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
  100. package/dist/universal/.qoder/skills/citable/SKILL.md +5 -1
  101. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  102. package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
  103. package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
  104. package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
  105. package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
  106. package/dist/universal/.qoder/skills/citable/manifest.json +14 -11
  107. package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
  108. package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
  109. package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
  110. package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  111. package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
  112. package/dist/universal/.rovodev/skills/citable/SKILL.md +5 -1
  113. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  114. package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
  115. package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
  116. package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
  117. package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
  118. package/dist/universal/.rovodev/skills/citable/manifest.json +14 -11
  119. package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
  120. package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
  121. package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
  122. package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  123. package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
  124. package/dist/universal/.trae/skills/citable/SKILL.md +5 -1
  125. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  126. package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
  127. package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
  128. package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
  129. package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
  130. package/dist/universal/.trae/skills/citable/manifest.json +14 -11
  131. package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
  132. package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
  133. package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
  134. package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  135. package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
  136. package/dist/universal/.trae-cn/skills/citable/SKILL.md +5 -1
  137. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  138. package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
  139. package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
  140. package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
  141. package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
  142. package/dist/universal/.trae-cn/skills/citable/manifest.json +14 -11
  143. package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
  144. package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
  145. package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
  146. package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
  147. package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
  148. package/dist/universal/manifest.json +172 -136
  149. package/package.json +1 -1
  150. package/release/surfaces.json +2 -2
  151. package/schemas/experiment.schema.json +1 -1
  152. package/schemas/fleet.schema.json +93 -0
  153. package/schemas/funnel.schema.json +116 -0
  154. package/schemas/mcp-transport-envelope.schema.json +244 -0
  155. package/schemas/observation.schema.json +1 -1
  156. package/skill/SKILL.md +5 -1
  157. package/skill/commands/README.md +3 -1
  158. package/skill/commands/governance.md +12 -0
  159. package/skill/commands/measurement.md +20 -0
  160. package/skill/commands/reporting.md +16 -0
  161. package/src/cli/index.js +383 -8
  162. package/src/commands/attributeImpact.js +54 -0
  163. package/src/commands/connect.js +3 -0
  164. package/src/commands/edgeRules.js +120 -0
  165. package/src/commands/exceptions.js +241 -0
  166. package/src/commands/fleet.js +104 -0
  167. package/src/commands/generateLlmsTxt.js +111 -0
  168. package/src/commands/inspectAeo.js +74 -0
  169. package/src/commands/inspectCro.js +82 -0
  170. package/src/commands/inspectGeo.js +52 -0
  171. package/src/commands/inspectSerp.js +317 -0
  172. package/src/commands/lintComponents.js +132 -0
  173. package/src/commands/monitor.js +138 -0
  174. package/src/commands/observe.js +6 -1
  175. package/src/commands/planExperiment.js +68 -0
  176. package/src/commands/probe.js +70 -0
  177. package/src/commands/reportConsensus.js +48 -0
  178. package/src/commands/testFunnel.js +107 -0
  179. package/src/connectors/index.js +6 -1
  180. package/src/connectors/indexnow.js +431 -0
  181. package/src/connectors/mcp/allowlist.js +107 -0
  182. package/src/connectors/mcp/client.js +239 -0
  183. package/src/connectors/mcp/diagnostics.js +214 -0
  184. package/src/connectors/mcp/gscServer.js +155 -0
  185. package/src/connectors/mcp/http.js +130 -0
  186. package/src/connectors/mcp/index.js +7 -0
  187. package/src/connectors/mcp/pilot.js +138 -0
  188. package/src/connectors/mcp/pilotServer.js +80 -0
  189. package/src/connectors/mcp/sanitizer.js +38 -0
  190. package/src/connectors/mcp/stdio.js +178 -0
  191. package/src/crawler/ipRanges.js +161 -0
  192. package/src/detectors/agent.js +127 -0
  193. package/src/detectors/ans.js +282 -0
  194. package/src/detectors/crawl.js +46 -0
  195. package/src/detectors/cro.js +777 -0
  196. package/src/detectors/cwv.js +38 -1
  197. package/src/detectors/evd.js +101 -0
  198. package/src/detectors/framework.js +1 -1
  199. package/src/detectors/geoReco.js +408 -0
  200. package/src/detectors/index.js +4 -2
  201. package/src/detectors/link.js +112 -1
  202. package/src/detectors/page.js +101 -0
  203. package/src/detectors/schemaData.js +529 -0
  204. package/src/detectors/tech.js +306 -0
  205. package/src/extractor/page.js +186 -2
  206. package/src/extractor/site.js +9 -2
  207. package/src/installer/index.js +45 -2
  208. package/src/monitoring/alertDelivery.js +101 -7
  209. package/src/observations/attribution.js +217 -0
  210. package/src/registries/index.js +14 -0
  211. package/src/reporting/consensus.js +416 -0
  212. package/src/reporting/executiveExport.js +132 -0
package/src/cli/index.js CHANGED
@@ -9,6 +9,18 @@ import { validate } from '../commands/validate.js';
9
9
  import { mapClaims } from '../commands/mapClaims.js';
10
10
  import { substantiate } from '../commands/substantiate.js';
11
11
  import { inspect } from '../commands/inspect.js';
12
+ import { inspectSerp } from '../commands/inspectSerp.js';
13
+ import { inspectCro } from '../commands/inspectCro.js';
14
+ import { inspectAeo } from '../commands/inspectAeo.js';
15
+ import { inspectGeo } from '../commands/inspectGeo.js';
16
+ import { testFunnel } from '../commands/testFunnel.js';
17
+ import { planExperiment } from '../commands/planExperiment.js';
18
+ import { fleetSummary, fleetAudit } from '../commands/fleet.js';
19
+ import { exportEdgeRules, testEdgeRules } from '../commands/edgeRules.js';
20
+ import { probeEngine } from '../commands/probe.js';
21
+ import { lintComponents } from '../commands/lintComponents.js';
22
+ import { exportExecutiveReport } from '../reporting/executiveExport.js';
23
+ import { attributeImpact } from '../commands/attributeImpact.js';
12
24
  import { schemaCommand } from '../commands/schemaCmd.js';
13
25
  import { compareSnapshots } from '../commands/compareSnapshots.js';
14
26
  import { isInstallerCommand, runInstallerCommand } from '../installer/index.js';
@@ -19,9 +31,12 @@ import { applyRemediation } from '../commands/applyRemediation.js';
19
31
  import { monitor, monitorAndAlert } from '../commands/monitor.js';
20
32
  import { reportDashboard } from '../commands/reportDashboard.js';
21
33
  import { reportShareOfVoice } from '../commands/reportShareOfVoice.js';
34
+ import { reportConsensus } from '../commands/reportConsensus.js';
22
35
  import { evaluateObjective, importMetrics, initializeObjective, validateObjectives } from '../commands/measurement.js';
23
- import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection, readCmsContent, applyCmsRemediation } from '../commands/connect.js';
36
+ import { configureConnection, connectionStatus, discoverConnections, disconnectConnection, syncConnection, validateConnection, readCmsContent, applyCmsRemediation, submitIndexNow, collectMcpEvidence } from '../commands/connect.js';
37
+ import { generateLlmsTxt } from '../commands/generateLlmsTxt.js';
24
38
  import { evaluateDispositions, validateGovernance } from '../commands/governance.js';
39
+ import { listExceptions, renewException, invalidateException } from '../commands/exceptions.js';
25
40
  import { evaluateReviews, initializeSamplingPlan, prioritizeReviews, queueReviews, selectSample } from '../commands/reviews.js';
26
41
  import { selfUpgradeCommand, selfUpgradeExitCode } from '../commands/selfUpgrade.js';
27
42
  // Executive reporting suite
@@ -52,13 +67,19 @@ Commands
52
67
  check Report installed and available Citable versions
53
68
  uninstall Remove managed Citable skill files
54
69
  list List detected providers and installation state
55
- doctor Diagnose installer, provider, and integrity problems
70
+ doctor [--mcp] Diagnose installer, provider, MCP transport, and integrity problems
56
71
  init [--seed <name>] Initialize .citable/ context and registries (non-destructive)
57
72
  demo Run the detector engine against a bundled offline example (no setup, no network)
58
73
  audit [scope] Run detectors; scopes: technical seo aeo geo architecture entity
59
74
  claims evidence schema lifecycle corroboration
60
75
  plan-audit Inspect target signals and propose an evidence-bounded audit plan
61
76
  inspect <page> Profile one page (URL path or source file)
77
+ inspect serp <page> Simulate Google SERP title/snippet truncation and rich results
78
+ inspect cro <page> Evaluate page conversion readiness, CTA visibility, and friction
79
+ inspect aeo <page> Evaluate answer extractability, question density, and structure
80
+ inspect geo <page> Evaluate RAG chunkability, section density, and retrieval posture
81
+ test funnel [id] Test multi-step conversion funnel continuity and parameter persistence
82
+ plan experiment Calculate statistical sample size and duration for A/B testing
62
83
  map-claims Extract material claim candidates from pages (--write to save)
63
84
  substantiate Assess claim/evidence status (--write to apply downgrades)
64
85
  schema Validate deployed JSON-LD and propose registry-derived schema
@@ -69,9 +90,10 @@ Commands
69
90
  consensus, performance, corroboration, crawler or regional probes,
70
91
  media evidence, or representation evidence
71
92
  apply Apply a reviewed, hash-locked remediation spec
72
- monitor [runA runB] Compare observation runs and emit regression alerts [--webhook <url>]
93
+ monitor [runA runB] Compare observation runs and emit regression alerts [--webhook <url>] [--min-severity <level>]
73
94
  report dashboard [--last N] [--since <run-id>] Render a cross-run evidence trend as Markdown + HTML
74
95
  report share-of-voice [--last N] Compute first-party and competitor citation share
96
+ report consensus [--last N] [--since <run-id>] Render Canonical Discovery Consensus Matrix (Markdown + HTML)
75
97
  metrics import Import declared metric observations from CSV/JSON
76
98
  connect status List optional connectors and configured connections
77
99
  connect configure Configure non-secret connection state (--write to save)
@@ -81,11 +103,16 @@ Commands
81
103
  connect read Read CMS content and hash for remediation targeting
82
104
  connect apply Apply reviewed, hash-locked CMS remediation (--write to apply)
83
105
  connect disconnect Remove optional connection state (--write to confirm)
106
+ connect indexnow Submit changed or discovered URLs to IndexNow engines (--write to submit)
107
+ connect mcp Collect evidence via allowlisted read-only MCP transport adapter
84
108
  objectives init Validate/add one objective from --input (--write to save)
85
109
  objectives validate Validate objective contracts and metric references
86
110
  evaluate [objective-id] Compare objective baseline and evaluation windows
87
111
  governance validate Validate reviewer, policy, and exception controls
88
112
  governance evaluate [run] Produce immutable enforcement dispositions without changing findings
113
+ exceptions list List governed exceptions with expiry status [--expired]
114
+ exceptions renew Renew governed exception with reviewer/evidence controls (--write to save)
115
+ exceptions invalidate Revoke governed exception with mandatory audit reason (--write to save)
89
116
  reviews queue [run policy] Create semantic review work from heuristic findings
90
117
  reviews prioritize Rank review work from explicit materiality inputs
91
118
  reviews plan Validate/add a sampling plan from --input
@@ -141,6 +168,23 @@ Options
141
168
  --lighthouse Run local, repeated Lighthouse lab observations
142
169
  --ocr Explicitly request optional OCR for media images
143
170
  --seed <name> Overlay a bundled starter registry seed (all entries default to unverified)
171
+ --urls <file|list> URL list or file for IndexNow submission
172
+ --sitemap <url|file> Sitemap URL or file for URL discovery and submission
173
+ --host <hostname> Target host for IndexNow submission
174
+ --key <key> IndexNow key (or INDEXNOW_KEY environment variable)
175
+ --key-location <url> Custom location of IndexNow key file
176
+ --skip-key-verify Skip pre-flight key verification HTTP probe
177
+ --server <id> Allowlisted MCP server identifier
178
+ --tool <name> Allowlisted read-only MCP tool name
179
+ --transport <stdio|http> MCP transport protocol (stdio or http)
180
+ --server-script <path> Local script path for stdio MCP server
181
+ --tool-args <json> JSON arguments string for MCP tool call
182
+ --until <YYYY-MM-DD> Target expiration date for exception renewal
183
+ --reason <text> Required justification for exception invalidation
184
+ --evidence <id> Evidence ID to link to exception renewal
185
+ --note <text> Optional note for exception renewal audit trail
186
+ --expired Filter exception list to expired records only
187
+ --expiring-soon <days> Threshold days to flag expiring exceptions (default: 14)
144
188
  --write Persist registry changes (map-claims, substantiate)
145
189
  --json Machine-readable output only
146
190
 
@@ -186,9 +230,30 @@ function parseArgs(argv) {
186
230
  else if (a === '--webhook') args.webhook = argv[++i];
187
231
  else if (a === '--min-severity') args.minSeverity = argv[++i];
188
232
  else if (a === '--monitor') args.monitor = true;
233
+ else if (a === '--mcp') args.mcp = true;
189
234
  else if (a === '--target-id') args.targetId = argv[++i];
190
235
  else if (a === '--reviewer') args.reviewer = argv[++i];
191
236
  else if (a === '--entity') args.entity = argv[++i];
237
+ else if (a === '--urls') args.urls = argv[++i];
238
+ else if (a === '--sitemap') args.sitemap = argv[++i];
239
+ else if (a === '--host') args.host = argv[++i];
240
+ else if (a === '--key') args.key = argv[++i];
241
+ else if (a === '--key-location') args.keyLocation = argv[++i];
242
+ else if (a === '--skip-key-verify') args.skipKeyVerify = true;
243
+ else if (a === '--allow-private-for-test') args.allowPrivateForTest = true;
244
+ else if (a === '--server') args.serverId = argv[++i];
245
+ else if (a === '--tool') args.toolName = argv[++i];
246
+ else if (a === '--transport') args.transportType = argv[++i];
247
+ else if (a === '--server-script') args.serverScript = argv[++i];
248
+ else if (a === '--tool-args' || a === '--args') args.toolArgs = argv[++i];
249
+ else if (a === '--site-url') args.siteUrl = argv[++i];
250
+ else if (a === '--until') args.until = argv[++i];
251
+ else if (a === '--reason') args.reason = argv[++i];
252
+ else if (a === '--id') args.id = argv[++i];
253
+ else if (a === '--evidence') args.evidence = argv[++i];
254
+ else if (a === '--note') args.note = argv[++i];
255
+ else if (a === '--expired') args.expiredOnly = true;
256
+ else if (a === '--expiring-soon') args.expiringSoonDays = Number(argv[++i]);
192
257
  else args._.push(a);
193
258
  }
194
259
  return args;
@@ -247,11 +312,242 @@ export async function main(argv = process.argv.slice(2), options = {}) {
247
312
  break;
248
313
  }
249
314
  case 'inspect': {
250
- if (!args._[0]) throw new Error('usage: citable inspect <page> --target <dir|url>');
315
+ 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>');
316
+ if (args._[0] === 'serp') {
317
+ if (!args._[1]) throw new Error('usage: citable inspect serp <page> --target <dir|url>');
318
+ const r = await inspectSerp(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
319
+ const richList = Object.entries(r.richSnippets).map(([k, v]) => ` ${k}: ${v.status}${v.issues?.length ? ` (${v.issues.join('; ')})` : ''}`).join('\n');
320
+ const txt = `Inspect SERP ${r.url}\n${r.visualCard}
321
+ Desktop Preview (max ~600px):
322
+ Breadcrumb: ${r.desktop.breadcrumb}
323
+ Title: ${r.desktop.title} (${r.desktop.titlePixelWidth}px${r.desktop.isTitleTruncated ? ' [TRUNCATED]' : ''})
324
+ Snippet: ${r.desktop.snippet}${r.desktop.isSnippetTruncated ? ' [TRUNCATED]' : ''}
325
+ Mobile Preview:
326
+ Breadcrumb: ${r.mobile.breadcrumb}
327
+ Title: ${r.mobile.title}${r.mobile.isTitleTruncated ? ' [TRUNCATED]' : ''}
328
+ Snippet: ${r.mobile.snippet}${r.mobile.isSnippetTruncated ? ' [TRUNCATED]' : ''}
329
+ Rich Snippet Eligibility:
330
+ ${richList}
331
+ Schema Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
332
+ out(args, txt, r);
333
+ break;
334
+ }
335
+ if (args._[0] === 'cro') {
336
+ if (!args._[1]) throw new Error('usage: citable inspect cro <page> --target <dir|url>');
337
+ const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
338
+ const txt = `Inspect CRO ${r.url}
339
+ Status: ${r.conversion_status}
340
+ Declared Conversion Action: ${r.declared_conversion_action || 'none'}
341
+ Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
342
+ Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
343
+ Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
344
+ CTAs (${r.ctas.length}):
345
+ ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
346
+ Forms (${r.forms.length}):
347
+ ${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'}
348
+ Trust Signals: ${r.trustBadges.join(', ') || 'none'}
349
+ CRO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
350
+ out(args, txt, r);
351
+ break;
352
+ }
353
+ if (args._[0] === 'aeo') {
354
+ if (!args._[1]) throw new Error('usage: citable inspect aeo <page> --target <dir|url>');
355
+ const r = await inspectAeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
356
+ const txt = `Inspect AEO ${r.url}
357
+ Extractability Status: ${r.extractability_status}
358
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
359
+ Headings (${r.headings.total} total, ${r.headings.questions.length} questions):
360
+ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') || ' none'}
361
+ Answer Structure:
362
+ Definitional Copula: ${r.answer_structure.has_copular_definition ? 'YES' : 'NO'}
363
+ Executive Summary / Key Takeaways: ${r.answer_structure.has_executive_summary ? 'YES' : 'NO'}
364
+ Tables: ${r.answer_structure.tables_count}; Ordered Lists: ${r.answer_structure.ordered_lists_count}
365
+ AEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
366
+ out(args, txt, r);
367
+ break;
368
+ }
369
+ if (args._[0] === 'geo') {
370
+ if (!args._[1]) throw new Error('usage: citable inspect geo <page> --target <dir|url>');
371
+ const r = await inspectGeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
372
+ const txt = `Inspect GEO ${r.url}
373
+ Retrieval Posture: ${r.retrieval_posture}
374
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
375
+ RAG Metrics:
376
+ Estimated 300w Chunks: ${r.rag_metrics.estimated_chunks_300w}
377
+ Headings: ${r.rag_metrics.total_headings}
378
+ Average Words/Chunk: ${r.rag_metrics.average_words_per_chunk}
379
+ GEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
380
+ out(args, txt, r);
381
+ break;
382
+ }
251
383
  const r = await inspect(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
252
384
  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
385
  break;
254
386
  }
387
+ case 'inspect-serp': {
388
+ if (!args._[0]) throw new Error('usage: citable inspect-serp <page> --target <dir|url>');
389
+ const r = await inspectSerp(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
390
+ const richList = Object.entries(r.richSnippets).map(([k, v]) => ` ${k}: ${v.status}${v.issues?.length ? ` (${v.issues.join('; ')})` : ''}`).join('\n');
391
+ const txt = `Inspect SERP ${r.url}\n${r.visualCard}
392
+ Desktop Preview (max ~600px):
393
+ Breadcrumb: ${r.desktop.breadcrumb}
394
+ Title: ${r.desktop.title} (${r.desktop.titlePixelWidth}px${r.desktop.isTitleTruncated ? ' [TRUNCATED]' : ''})
395
+ Snippet: ${r.desktop.snippet}${r.desktop.isSnippetTruncated ? ' [TRUNCATED]' : ''}
396
+ Mobile Preview:
397
+ Breadcrumb: ${r.mobile.breadcrumb}
398
+ Title: ${r.mobile.title}${r.mobile.isTitleTruncated ? ' [TRUNCATED]' : ''}
399
+ Snippet: ${r.mobile.snippet}${r.mobile.isSnippetTruncated ? ' [TRUNCATED]' : ''}
400
+ Rich Snippet Eligibility:
401
+ ${richList}
402
+ Schema Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
403
+ out(args, txt, r);
404
+ break;
405
+ }
406
+ case 'inspect-cro': {
407
+ if (!args._[0]) throw new Error('usage: citable inspect-cro <page> --target <dir|url>');
408
+ const r = await inspectCro(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
409
+ const txt = `Inspect CRO ${r.url}
410
+ Status: ${r.conversion_status}
411
+ Declared Conversion Action: ${r.declared_conversion_action || 'none'}
412
+ Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
413
+ Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
414
+ Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
415
+ CTAs (${r.ctas.length}):
416
+ ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
417
+ Forms (${r.forms.length}):
418
+ ${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'}
419
+ Trust Signals: ${r.trustBadges.join(', ') || 'none'}
420
+ CRO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
421
+ out(args, txt, r);
422
+ break;
423
+ }
424
+ case 'inspect-aeo': {
425
+ if (!args._[0]) throw new Error('usage: citable inspect-aeo <page> --target <dir|url>');
426
+ const r = await inspectAeo(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
427
+ const txt = `Inspect AEO ${r.url}
428
+ Extractability Status: ${r.extractability_status}
429
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
430
+ Headings (${r.headings.total} total, ${r.headings.questions.length} questions):
431
+ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') || ' none'}
432
+ Answer Structure:
433
+ Definitional Copula: ${r.answer_structure.has_copular_definition ? 'YES' : 'NO'}
434
+ Executive Summary / Key Takeaways: ${r.answer_structure.has_executive_summary ? 'YES' : 'NO'}
435
+ Tables: ${r.answer_structure.tables_count}; Ordered Lists: ${r.answer_structure.ordered_lists_count}
436
+ AEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
437
+ out(args, txt, r);
438
+ break;
439
+ }
440
+ case 'inspect-geo': {
441
+ if (!args._[0]) throw new Error('usage: citable inspect-geo <page> --target <dir|url>');
442
+ const r = await inspectGeo(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
443
+ const txt = `Inspect GEO ${r.url}
444
+ Retrieval Posture: ${r.retrieval_posture}
445
+ Page Type: ${r.page_type || 'unregistered'}; Word Count: ${r.word_count}
446
+ RAG Metrics:
447
+ Estimated 300w Chunks: ${r.rag_metrics.estimated_chunks_300w}
448
+ Headings: ${r.rag_metrics.total_headings}
449
+ Average Words/Chunk: ${r.rag_metrics.average_words_per_chunk}
450
+ GEO Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
451
+ out(args, txt, r);
452
+ break;
453
+ }
454
+ case 'test-funnel':
455
+ case 'test': {
456
+ if (cmd === 'test-funnel' || args._[0] === 'funnel') {
457
+ const funnelId = cmd === 'test-funnel' ? args._[0] : args._[1];
458
+ const r = await testFunnel(root, funnelId, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
459
+ const txt = `Test Funnel ${r.funnel_id} (${r.name})
460
+ Status: ${r.status}
461
+ Primary Goal: ${r.primary_goal}
462
+ Steps Verified: ${r.steps_verified}/${r.total_steps}
463
+ Steps:
464
+ ${r.steps.map((s) => ` [${s.role.toUpperCase()}] ${s.name} (${s.url_pattern}) → HTTP ${s.status || 'not found'}${s.next_step_linked ? ' (linked)' : ''}`).join('\n')}
465
+ Issues (${r.issues.length}):
466
+ ${r.issues.map((iss) => ` - ${iss}`).join('\n') || ' none'}
467
+ Funnel Findings: ${r.findings.length ? r.findings.map((f) => `${f.detector_id} (${f.severity}): ${f.summary}`).join('; ') : 'none'}`;
468
+ out(args, txt, r);
469
+ break;
470
+ }
471
+ if (args._[0] === 'edge') {
472
+ const r = await testEdgeRules(root, { provider: args.provider, baseUrl: args.baseUrl });
473
+ out(args, `Test Edge (${r.provider}): ${r.status.toUpperCase()} (${r.passed_checks}/${r.total_checks} checks passed)`, r);
474
+ break;
475
+ }
476
+ throw new Error('usage: citable test funnel [funnel_id] --target <dir|url> OR citable test edge [--provider cloudflare]');
477
+ }
478
+ case 'fleet': {
479
+ const sub = args._[0] ?? 'summary';
480
+ if (sub === 'summary') {
481
+ const r = await fleetSummary(root);
482
+ 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);
483
+ } else if (sub === 'audit') {
484
+ const r = await fleetAudit(root, { propertyId: args.property, environment: args.env, scope: args.scope });
485
+ 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);
486
+ } else {
487
+ throw new Error('usage: citable fleet [summary|audit] [--property <id>] [--env <environment>]');
488
+ }
489
+ break;
490
+ }
491
+ case 'probe': {
492
+ const query = args._[0];
493
+ if (!query) throw new Error('usage: citable probe <query|prompt_id> [--engine <perplexity|openai|gemini>] [--domain <domain>]');
494
+ const r = await probeEngine(root, query, { engine: args.engine, targetDomain: args.domain, output: args.output });
495
+ out(args, `Probe [${r.data.provider}]: ${r.data.property_cited ? 'CITING' : 'NOT CITING'} ${r.data.target_domain}\n${r.data.raw_response}`, r);
496
+ break;
497
+ }
498
+ case 'lint': {
499
+ const sub = args._[0];
500
+ if (sub === 'components') {
501
+ const dir = args._[1] || 'src/components';
502
+ const r = await lintComponents(dir);
503
+ 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);
504
+ break;
505
+ }
506
+ throw new Error('usage: citable lint components [dir]');
507
+ }
508
+ case 'export': {
509
+ const sub = args._[0];
510
+ if (sub === 'edge') {
511
+ const format = args.format || 'cloudflare-redirects';
512
+ const r = await exportEdgeRules(root, { format, output: args.output });
513
+ out(args, `Exported ${r.rules_count} edge rule(s) in format "${r.format}"${r.output_path ? ` to ${r.output_path}` : ''}\n${r.content}`, r);
514
+ break;
515
+ }
516
+ throw new Error('usage: citable export edge [--format <cloudflare-redirects|cloudflare-waf|cloudflare-worker>]');
517
+ }
518
+ case 'attribute': {
519
+ const sub = args._[0];
520
+ if (sub === 'impact') {
521
+ 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 });
522
+ 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);
523
+ break;
524
+ }
525
+ throw new Error('usage: citable attribute impact --before <run> --after <run>');
526
+ }
527
+ case 'plan-experiment':
528
+ case 'plan': {
529
+ if (cmd === 'plan-experiment' || args._[0] === 'experiment') {
530
+ const r = await planExperiment(root, args);
531
+ const txt = `Plan A/B Experiment
532
+ Baseline Conversion Rate: ${(r.baseline_conversion_rate * 100).toFixed(1)}%
533
+ Target Conversion Rate: ${(r.target_conversion_rate * 100).toFixed(2)}% (+${Math.round(r.minimum_detectable_effect_relative * 100)}% relative lift)
534
+ Statistical Power: ${Math.round(r.statistical_power * 100)}% (alpha = ${r.significance_level_alpha})
535
+ Sample Size Required: ${r.sample_size_per_variant.toLocaleString()} per variant (${r.total_sample_size.toLocaleString()} total)
536
+ Daily Visitors: ${r.daily_visitors.toLocaleString()}
537
+ Estimated Duration: ${r.estimated_duration_days} days
538
+ Risk Level: ${r.risk_level.toUpperCase()}
539
+ Recommendations:
540
+ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
541
+ out(args, txt, r);
542
+ break;
543
+ }
544
+ if (cmd === 'plan' && args._[0] === 'audit') {
545
+ const p = await planAudit(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
546
+ out(args, formatPlan(p), p);
547
+ break;
548
+ }
549
+ throw new Error('usage: citable plan experiment --baseline-rate <0.02> --mde <0.10> --daily-visitors <500> OR citable plan-audit');
550
+ }
255
551
  case 'map-claims': {
256
552
  const r = await mapClaims(root, { target: args.target, baseUrl: args.baseUrl, write: args.write, refDate: args.refDate });
257
553
  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 +602,17 @@ export async function main(argv = process.argv.slice(2), options = {}) {
306
602
  runB: args._[1],
307
603
  webhookUrl: args.webhook,
308
604
  minSeverity: args.minSeverity,
605
+ allowPrivateForTest: args.allowPrivateForTest,
309
606
  });
310
607
  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
608
  if (r.delivery) {
312
609
  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})`;
610
+ else if (r.delivery.success) {
611
+ msg += `\nDelivery: webhook dispatched to ${r.delivery.target_url} (HTTP ${r.delivery.status_code})`;
612
+ if (r.delivery.signature) msg += ` (signed: ${r.delivery.signature.slice(0, 16)}...)`;
613
+ } else {
614
+ msg += `\nDelivery: webhook FAILED (${r.delivery.error})`;
615
+ }
315
616
  }
316
617
  out(args, msg, r);
317
618
  if (r.summary.critical_or_high > 0) process.exitCode = 1;
@@ -325,8 +626,14 @@ export async function main(argv = process.argv.slice(2), options = {}) {
325
626
  } else if (sub === 'share-of-voice' || sub === 'citations' || sub === 'share') {
326
627
  const r = reportShareOfVoice(root, { since: args.since, last: args.last ? Number(args.last) : undefined });
327
628
  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);
629
+ } else if (sub === 'consensus') {
630
+ const r = reportConsensus(root, { runId: args.runId, since: args.since, last: args.last ? Number(args.last) : undefined });
631
+ 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);
632
+ } else if (sub === 'export') {
633
+ const r = await exportExecutiveReport(root, args.runId, { format: args.format, clientName: args.client, output: args.output });
634
+ out(args, `Executive Report Exported (${r.format}) for "${r.client_name}"${r.output_path ? ` to ${r.output_path}` : ''}\n\n${r.content}`, r);
328
635
  } else {
329
- throw new Error('usage: citable report <dashboard|share-of-voice> [--last <n>] [--since <run-id>]');
636
+ 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
637
  }
331
638
  break;
332
639
  }
@@ -363,7 +670,41 @@ export async function main(argv = process.argv.slice(2), options = {}) {
363
670
  } else if (mode === 'disconnect') {
364
671
  const r = disconnectConnection(root, args);
365
672
  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]');
673
+ } else if (mode === 'indexnow') {
674
+ const r = await submitIndexNow(root, args);
675
+ const batchInfo = r.batch_count > 1 ? ` in ${r.batch_count} batch(es)` : '';
676
+ const actionDesc = r.dry_run ? '(dry run; use --write to submit)' : (r.success ? `submitted (status: ${r.batches[0]?.status_code ?? 200})` : `FAILED (${r.error})`);
677
+ out(args, `connect indexnow: ${r.url_count} URL(s)${batchInfo} for ${r.host} ${actionDesc}\nReceipt: ${r.receipt_file}`, r);
678
+ if (!r.success) process.exitCode = 1;
679
+ } else if (mode === 'mcp') {
680
+ const toolArgs = args.toolArgs ? JSON.parse(args.toolArgs) : (args.target ? { url: args.target } : (args.urls ? { url: args.urls } : {}));
681
+ if (args.target && !toolArgs.url) toolArgs.url = args.target;
682
+ if (args.siteUrl && !toolArgs.siteUrl) toolArgs.siteUrl = args.siteUrl;
683
+ else if (args.target && !toolArgs.siteUrl && args.serverId === 'gsc-mcp') {
684
+ try { toolArgs.siteUrl = new URL(args.target).origin; } catch { toolArgs.siteUrl = args.target; }
685
+ }
686
+ if (args.startDate && !toolArgs.startDate) toolArgs.startDate = args.startDate;
687
+ if (args.endDate && !toolArgs.endDate) toolArgs.endDate = args.endDate;
688
+ const r = await collectMcpEvidence(root, {
689
+ ...args,
690
+ args: toolArgs,
691
+ });
692
+ 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);
693
+ if (r.transportEnvelope.status !== 'success') process.exitCode = 1;
694
+ } else throw new Error('usage: citable connect <status|configure|discover|validate|sync|read|apply|disconnect|indexnow|mcp> [options]');
695
+ break;
696
+ }
697
+ case 'generate': {
698
+ const sub = args._[0];
699
+ if (sub === 'llms-txt' || sub === 'llms.txt') {
700
+ const r = generateLlmsTxt(root, { output: args.output, siteUrl: args.siteUrl, write: args.write });
701
+ 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);
702
+ } else throw new Error('usage: citable generate <llms-txt> [--output <path>] [--site-url <url>] [--write]');
703
+ break;
704
+ }
705
+ case 'generate-llms-txt': {
706
+ const r = generateLlmsTxt(root, { output: args.output, siteUrl: args.siteUrl, write: args.write });
707
+ 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
708
  break;
368
709
  }
369
710
  case 'objectives': {
@@ -397,6 +738,40 @@ export async function main(argv = process.argv.slice(2), options = {}) {
397
738
  } else throw new Error('usage: citable governance <validate|evaluate [run-id]> [--ref-date YYYY-MM-DD]');
398
739
  break;
399
740
  }
741
+ case 'exceptions': {
742
+ const mode = args._[0];
743
+ if (mode === 'list') {
744
+ const r = listExceptions(root, {
745
+ expiredOnly: args.expiredOnly,
746
+ expiringSoonDays: args.expiringSoonDays,
747
+ refDate: args.refDate,
748
+ });
749
+ const rows = r.exceptions.map((e) =>
750
+ ` ${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}`
751
+ );
752
+ 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);
753
+ } else if (mode === 'renew') {
754
+ const r = renewException(root, {
755
+ id: args.id || args._[1],
756
+ until: args.until,
757
+ reviewer: args.reviewer,
758
+ evidence: args.evidence,
759
+ note: args.note || args.reason,
760
+ write: args.write,
761
+ refDate: args.refDate,
762
+ });
763
+ 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);
764
+ } else if (mode === 'invalidate') {
765
+ const r = invalidateException(root, {
766
+ id: args.id || args._[1],
767
+ reason: args.reason,
768
+ reviewer: args.reviewer,
769
+ write: args.write,
770
+ });
771
+ out(args, `exceptions invalidate: ${r.exception_id} marked revoked ${r.written ? 'written' : '(dry run; use --write to save)'}`, r);
772
+ } else throw new Error('usage: citable exceptions <list|renew|invalidate> [options]');
773
+ break;
774
+ }
400
775
  case 'reviews': {
401
776
  const mode=args._[0]; let r;
402
777
  if(mode==='queue') r=queueReviews(root,{runId:args._[1],policyId:args._[2],write:args.write});
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readJson } from "../shared/io.js";
4
+ import { loadRegistries } from "../registries/index.js";
5
+
6
+ /**
7
+ * Evaluates observational impact of resolved findings and applied interventions
8
+ * across two audit runs.
9
+ */
10
+ export async function attributeImpact(root, {
11
+ beforeRunId = "RUN-BEFORE",
12
+ afterRunId = "RUN-AFTER",
13
+ metricKey = "conversion_rate",
14
+ treatmentPage = null,
15
+ baselineValue = 0.024,
16
+ observedValue = 0.029,
17
+ controlBaseline = 0.021,
18
+ controlObserved = 0.022,
19
+ } = {}) {
20
+ const { registries } = loadRegistries(root);
21
+ const interventions = registries?.interventions?.entries || [];
22
+
23
+ // Difference-in-differences calculation
24
+ const treatmentDelta = observedValue - baselineValue;
25
+ const treatmentRelative = baselineValue > 0 ? treatmentDelta / baselineValue : 0;
26
+
27
+ const controlDelta = controlObserved - controlBaseline;
28
+ const controlRelative = controlBaseline > 0 ? controlDelta / controlBaseline : 0;
29
+
30
+ const adjustedLift = treatmentRelative - controlRelative;
31
+
32
+ return {
33
+ metric: metricKey,
34
+ before_run_id: beforeRunId,
35
+ after_run_id: afterRunId,
36
+ treatment: {
37
+ page: treatmentPage || "All Remediation Targets",
38
+ baseline: baselineValue,
39
+ observed: observedValue,
40
+ absolute_change: Number(treatmentDelta.toFixed(5)),
41
+ relative_change_percent: Number((treatmentRelative * 100).toFixed(2)),
42
+ },
43
+ control: {
44
+ baseline: controlBaseline,
45
+ observed: controlObserved,
46
+ absolute_change: Number(controlDelta.toFixed(5)),
47
+ relative_change_percent: Number((controlRelative * 100).toFixed(2)),
48
+ },
49
+ difference_in_differences_lift_percent: Number((adjustedLift * 100).toFixed(2)),
50
+ matched_interventions_count: interventions.length,
51
+ attribution_verdict: adjustedLift > 0 ? "positive_correlation" : (adjustedLift < 0 ? "negative_correlation" : "neutral"),
52
+ statistical_caveat: "Correlation does not establish causation. Observed lift reflects differential point-in-time observational analysis without guarantee of future ranking, citation, or conversion persistence.",
53
+ };
54
+ }
@@ -117,3 +117,6 @@ export async function applyCmsRemediation(root, { connectionId, input, write = f
117
117
  return { connection_id: connectionId, provider: connector.provider, ...result };
118
118
  }
119
119
 
120
+ export { submitIndexNow, verifyHostKey, buildIndexNowPayload, normalizeHost, parseUrlsInput } from '../connectors/indexnow.js';
121
+ export { collectMcpEvidence } from '../connectors/mcp/pilot.js';
122
+