@nebulacomponents/citable 1.16.0 → 1.18.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 (142) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +16 -3
  3. package/dist/universal/.agents/skills/citable/SKILL.md +66 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +9 -6
  6. package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  7. package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
  8. package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
  9. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +92 -0
  10. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  11. package/dist/universal/.claude/skills/citable/SKILL.md +66 -1
  12. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  13. package/dist/universal/.claude/skills/citable/manifest.json +9 -6
  14. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  15. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  16. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  17. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +92 -0
  18. package/dist/universal/.cursor/skills/citable/SKILL.md +66 -1
  19. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  20. package/dist/universal/.cursor/skills/citable/manifest.json +9 -6
  21. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  22. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  23. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  24. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +92 -0
  25. package/dist/universal/.gemini/skills/citable/SKILL.md +66 -1
  26. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  27. package/dist/universal/.gemini/skills/citable/manifest.json +9 -6
  28. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  29. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  30. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  31. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +92 -0
  32. package/dist/universal/.github/skills/citable/SKILL.md +66 -1
  33. package/dist/universal/.github/skills/citable/VERSION +1 -1
  34. package/dist/universal/.github/skills/citable/manifest.json +9 -6
  35. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  36. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  37. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  38. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +92 -0
  39. package/dist/universal/.kiro/skills/citable/SKILL.md +66 -1
  40. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  41. package/dist/universal/.kiro/skills/citable/manifest.json +9 -6
  42. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  43. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  44. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  45. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +92 -0
  46. package/dist/universal/.opencode/skills/citable/SKILL.md +66 -1
  47. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  48. package/dist/universal/.opencode/skills/citable/manifest.json +9 -6
  49. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  50. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  51. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  52. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +92 -0
  53. package/dist/universal/.pi/agent/skills/citable/SKILL.md +66 -1
  54. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  55. package/dist/universal/.pi/agent/skills/citable/manifest.json +9 -6
  56. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  57. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  58. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  59. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +92 -0
  60. package/dist/universal/.qoder/skills/citable/SKILL.md +66 -1
  61. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  62. package/dist/universal/.qoder/skills/citable/manifest.json +9 -6
  63. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  64. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  65. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  66. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +92 -0
  67. package/dist/universal/.rovodev/skills/citable/SKILL.md +66 -1
  68. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  69. package/dist/universal/.rovodev/skills/citable/manifest.json +9 -6
  70. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  71. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  72. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  73. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +92 -0
  74. package/dist/universal/.trae/skills/citable/SKILL.md +66 -1
  75. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  76. package/dist/universal/.trae/skills/citable/manifest.json +9 -6
  77. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  78. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  79. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  80. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +92 -0
  81. package/dist/universal/.trae-cn/skills/citable/SKILL.md +66 -1
  82. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  83. package/dist/universal/.trae-cn/skills/citable/manifest.json +9 -6
  84. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  85. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  86. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  87. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +92 -0
  88. package/dist/universal/manifest.json +112 -76
  89. package/package.json +1 -1
  90. package/schemas/cli-output-envelope.schema.json +30 -0
  91. package/schemas/finding.schema.json +69 -0
  92. package/schemas/remediation-verification.schema.json +98 -0
  93. package/schemas/sow.schema.json +92 -0
  94. package/skill/SKILL.md +66 -1
  95. package/src/analysis/behavioral.js +126 -0
  96. package/src/analysis/croAudit.js +302 -0
  97. package/src/analysis/croRoadmap.js +121 -0
  98. package/src/analysis/eeat.js +300 -0
  99. package/src/analysis/funnelAnalysis.js +171 -0
  100. package/src/analysis/iceMatrix.js +205 -0
  101. package/src/analysis/offpage.js +228 -0
  102. package/src/analysis/readiness.js +253 -0
  103. package/src/analysis/saliency.js +119 -0
  104. package/src/analysis/schemaRecommendations.js +173 -0
  105. package/src/analysis/strategicRoadmap.js +148 -0
  106. package/src/cli/index.js +418 -9
  107. package/src/commands/answerEngineReadiness.js +48 -0
  108. package/src/commands/audit.js +2 -2
  109. package/src/commands/auditBacklinks.js +79 -0
  110. package/src/commands/ciWorkflow.js +63 -0
  111. package/src/commands/compatibility.js +205 -0
  112. package/src/commands/context.js +2 -1
  113. package/src/commands/croBacklog.js +182 -0
  114. package/src/commands/croSuite.js +156 -0
  115. package/src/commands/edgeRules.js +80 -4
  116. package/src/commands/edgeSecurity.js +192 -0
  117. package/src/commands/executive.js +6 -0
  118. package/src/commands/executiveCro.js +30 -0
  119. package/src/commands/executiveSearch.js +28 -0
  120. package/src/commands/experimentGuardrails.js +213 -0
  121. package/src/commands/goldenCorpus.js +155 -0
  122. package/src/commands/implementationKit.js +202 -0
  123. package/src/commands/inspectCro.js +67 -0
  124. package/src/commands/inspectEeat.js +53 -0
  125. package/src/commands/planExperiment.js +52 -0
  126. package/src/commands/previewCro.js +241 -0
  127. package/src/commands/prioritize.js +42 -6
  128. package/src/commands/remediate.js +461 -0
  129. package/src/commands/roadmapCmd.js +69 -0
  130. package/src/commands/schemaCmd.js +10 -2
  131. package/src/commands/sowCmd.js +67 -0
  132. package/src/commands/sweep.js +357 -0
  133. package/src/commands/verifyRemediation.js +204 -0
  134. package/src/commands/visualRegression.js +165 -0
  135. package/src/components/index.js +556 -0
  136. package/src/detectors/cro.js +48 -0
  137. package/src/detectors/framework.js +31 -1
  138. package/src/reporting/executiveCroReport.js +731 -0
  139. package/src/reporting/executiveExport.js +175 -1
  140. package/src/reporting/executiveSearchReport.js +704 -0
  141. package/src/sow/admissibilityGate.js +194 -0
  142. package/src/sow/generateSow.js +762 -0
package/src/cli/index.js CHANGED
@@ -1,8 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { auditCroSuite, formatCroSuiteOutput } from "../commands/croSuite.js";
3
+ import { formatBacklogMarkdown } from "../commands/croBacklog.js";
4
+ import { formatCroRoadmapMarkdown } from "../analysis/croRoadmap.js";
5
+ import { sweepTechnical, formatSweepOutput } from "../commands/sweep.js";
6
+ import { inspectEeat, formatEeatOutput } from "../commands/inspectEeat.js";
7
+ import { inspectReadiness, formatReadinessOutput } from "../commands/answerEngineReadiness.js";
8
+ import { auditBacklinks, formatBacklinksOutput } from "../commands/auditBacklinks.js";
9
+ import { roadmapCommand } from "../commands/roadmapCmd.js";
10
+ import { sowCommand } from "../commands/sowCmd.js";
11
+ import { buildIceMatrix, formatIceMatrixOutput } from "../analysis/iceMatrix.js";
12
+ import fs from 'node:fs';
2
13
  import path from 'node:path';
3
14
  import { pathToFileURL } from 'node:url';
4
15
  import { init } from '../commands/init.js';
5
16
  import { audit } from '../commands/audit.js';
17
+ import { auditEdgeCode } from '../commands/edgeSecurity.js';
6
18
  import { planAudit } from '../commands/planAudit.js';
7
19
  import { demo } from '../commands/demo.js';
8
20
  import { validate } from '../commands/validate.js';
@@ -21,6 +33,14 @@ import { probeEngine } from '../commands/probe.js';
21
33
  import { lintComponents } from '../commands/lintComponents.js';
22
34
  import { exportExecutiveReport } from '../reporting/executiveExport.js';
23
35
  import { attributeImpact } from '../commands/attributeImpact.js';
36
+ import { remediateCommand } from '../commands/remediate.js';
37
+ import { verifyRemediation } from '../commands/verifyRemediation.js';
38
+ import { exportImplementationKit } from '../commands/implementationKit.js';
39
+ import { compatibilityCommand, verifyPage } from '../commands/compatibility.js';
40
+ import { runVisualRegression } from '../commands/visualRegression.js';
41
+ import { checkExperiment } from '../commands/experimentGuardrails.js';
42
+ import { runGoldenBenchmark } from '../commands/goldenCorpus.js';
43
+ import { previewCroCommand } from '../commands/previewCro.js';
24
44
  import { schemaCommand } from '../commands/schemaCmd.js';
25
45
  import { compareSnapshots } from '../commands/compareSnapshots.js';
26
46
  import { isInstallerCommand, runInstallerCommand } from '../installer/index.js';
@@ -76,12 +96,26 @@ Commands
76
96
  inspect <page> Profile one page (URL path or source file)
77
97
  inspect serp <page> Simulate Google SERP title/snippet truncation and rich results
78
98
  inspect cro <page> Evaluate page conversion readiness, CTA visibility, and friction
99
+ inspect saliency <page> Evaluate privacy-first algorithmic visual attention and CTA conspicuity
79
100
  inspect aeo <page> Evaluate answer extractability, question density, and structure
80
101
  inspect geo <page> Evaluate RAG chunkability, section density, and retrieval posture
102
+ preview cro <page> Interactive split-screen visual preview of original vs remediated page
103
+ remediate [finding] Synthesize accessible Nebula Components; safe patches with diff, validation, confidence, rollback (--write gated, fail closed)
81
104
  test funnel [id] Test multi-step conversion funnel continuity and parameter persistence
105
+ test visual Deterministic layout-contract checks + viewport/variant screenshot matrix (screenshots need playwright)
82
106
  plan experiment Calculate statistical sample size and duration for A/B testing
107
+ check experiment Experiment guardrails: SRM, stopping, power, contamination, lifecycle status
83
108
  map-claims Extract material claim candidates from pages (--write to save)
84
109
  substantiate Assess claim/evidence status (--write to apply downgrades)
110
+ cro [audit] Full CRO intelligence suite: funnel, ATF clarity, trust, cognitive load, ICE matrix, roadmap
111
+ cro backlog Generate A/B experimentation backlog with falsifiable hypotheses & guardrails
112
+ cro roadmap 30/90/180-day CRO roadmap tied to measurable conversion outcomes
113
+ sweep [technical] Run technical SEO sweep with Core Web Vitals metrics
114
+ inspect eeat <page> Evaluate on-page content against Google E-E-A-T rubric (0-5 scale)
115
+ inspect readiness <page> Evaluate answer-engine readiness across Perplexity, Bing Copilot, and ChatGPT
116
+ audit backlinks Audit off-page authority and identify toxic domains (--input <file>)
117
+ prioritize matrix Impact/Effort/Confidence (ICE) scoring matrix for roadmap & findings
118
+ roadmap [generate|show] 30/90/180-day strategic roadmap with milestone horizons
85
119
  schema Validate deployed JSON-LD and propose registry-derived schema
86
120
  validate [mode] registries (default) | claims | evidence | schema | links
87
121
  compare-snapshots [a b] Regression diff between two audit runs
@@ -94,6 +128,8 @@ Commands
94
128
  report dashboard [--last N] [--since <run-id>] Render a cross-run evidence trend as Markdown + HTML
95
129
  report share-of-voice [--last N] Compute first-party and competitor citation share
96
130
  report consensus [--last N] [--since <run-id>] Render Canonical Discovery Consensus Matrix (Markdown + HTML)
131
+ report search [--target <dir|url>] [--run <id>] Enterprise Search Intelligence & AEO/GEO Executive Briefing (19 pillars)
132
+ report cro [--target <dir|url>] [--input <file>] Enterprise CRO & Customer Journey Executive Briefing (25 pillars)
97
133
  metrics import Import declared metric observations from CSV/JSON
98
134
  connect status List optional connectors and configured connections
99
135
  connect configure Configure non-secret connection state (--write to save)
@@ -123,9 +159,14 @@ Commands
123
159
  corpus evaluate Evaluate a disclosed real-property acceptance corpus
124
160
  corpus publish Validate and project an owner-authorized public corpus
125
161
  corpus receipt Create a reproducibility receipt for a sealed run
162
+ corpus benchmark Run the labeled golden fixture corpus and report per-detector precision/recall
126
163
  corpus compare-receipts Compare two acceptance-run receipt envelopes
127
164
  artifacts export Export one sealed run as a portable verified directory
128
165
  artifacts verify Verify an exported artifact interchange directory
166
+ verify remediation Closed loop: re-run the detector after a patch and emit a before/after evidence bundle
167
+ verify page <page> Run all detectors scoped to one page and report posture (pass/attention/blocked)
168
+ compatibility Pre-flight: Node engine, optional adapters, browser, framework, registries, edge limits
169
+ kit export Customer-ready implementation kit: finding, diff, evidence, acceptance tests, deployment
129
170
  artifacts import Import a verified run without changing its canonical bytes
130
171
  self-upgrade Check for a newer version and upgrade the npx cache
131
172
  kpi [list|show|validate] KPI architecture — govern metric definitions, sources, targets
@@ -203,6 +244,7 @@ function parseArgs(argv) {
203
244
  else if (a === '--target') args.target = argv[++i];
204
245
  else if (a === '--base-url') args.baseUrl = argv[++i];
205
246
  else if (a === '--ref-date') args.refDate = argv[++i];
247
+ else if (a === '--viewport') args.viewport = argv[++i];
206
248
  else if (a === '--input') args.input = argv[++i];
207
249
  else if (a === '--output') args.output = argv[++i];
208
250
  else if (a === '--run') args.runId = argv[++i];
@@ -254,14 +296,59 @@ function parseArgs(argv) {
254
296
  else if (a === '--note') args.note = argv[++i];
255
297
  else if (a === '--expired') args.expiredOnly = true;
256
298
  else if (a === '--expiring-soon') args.expiringSoonDays = Number(argv[++i]);
299
+ else if (a === '--finding') args.finding = argv[++i];
300
+ else if (a === '--component') args.component = argv[++i];
301
+ else if (a === '--format') args.format = argv[++i];
302
+ else if (a === '--strict') args.strict = true;
303
+ else if (a === '--export') args.export = argv[++i];
304
+ else if (a === '--apply') args.apply = true;
305
+ else if (a === '--preview-file') args['preview-file'] = argv[++i];
306
+ else if (a === '--observed-control') args['observed-control'] = argv[++i];
307
+ else if (a === '--observed-variant') args['observed-variant'] = argv[++i];
308
+ else if (a === '--days-running') args['days-running'] = argv[++i];
309
+ else if (a === '--baseline-rate') args['baseline-rate'] = argv[++i];
310
+ else if (a === '--p-value') args['p-value'] = argv[++i];
311
+ else if (a === '--matrix') args.matrix = true;
312
+ else if (a === '--roadmap') args.roadmap = true;
313
+ else if (a === '--engines') args.engines = true;
314
+ else if (a === '--funnel') args.funnel = argv[++i];
315
+ else if (a === '--client') args.client = argv[++i];
316
+ else if (a === '--type') args.type = argv[++i];
317
+ else if (a === '--revenue-claimed') args['revenue-claimed'] = true;
257
318
  else args._.push(a);
258
319
  }
259
320
  return args;
260
321
  }
261
322
 
262
- function out(args, human, data) {
263
- if (args.json) console.log(JSON.stringify(data, null, 2));
264
- else console.log(human);
323
+ const TOOL_VERSION_CLI = JSON.parse(
324
+ fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
325
+ ).version;
326
+
327
+ // Set by main(); read by out() for the JSON envelope. Kept off `args` because
328
+ // several commands spread parsed args directly into option objects.
329
+ let CURRENT_COMMAND = null;
330
+
331
+ /**
332
+ * Stable machine-readable output contract (citable_output_schema 1.0):
333
+ * every command's --json payload is wrapped in this envelope so CI systems
334
+ * and agencies can consume results without parsing terminal text. The
335
+ * envelope version changes only on breaking envelope changes; command
336
+ * payloads evolve backward-compatibly inside `result`.
337
+ */
338
+ const OUTPUT_SCHEMA_VERSION = '1.0';
339
+
340
+ function out(args, human, data, command = null) {
341
+ if (args.json) {
342
+ console.log(JSON.stringify({
343
+ citable_output_schema: OUTPUT_SCHEMA_VERSION,
344
+ tool_version: TOOL_VERSION_CLI,
345
+ command: command ?? CURRENT_COMMAND,
346
+ generated_at: new Date().toISOString(),
347
+ result: data,
348
+ }, null, 2));
349
+ } else {
350
+ console.log(human);
351
+ }
265
352
  }
266
353
 
267
354
  export async function main(argv = process.argv.slice(2), options = {}) {
@@ -272,6 +359,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
272
359
  }
273
360
  if (isInstallerCommand(cmd)) return runInstallerCommand(cmd, argv.slice(1), options);
274
361
  const args = parseArgs(argv.slice(1));
362
+ CURRENT_COMMAND = cmd;
275
363
  const root = options.cwd ?? process.cwd();
276
364
 
277
365
  try {
@@ -299,8 +387,30 @@ export async function main(argv = process.argv.slice(2), options = {}) {
299
387
  break;
300
388
  }
301
389
  case 'audit': {
390
+ if (args._[0] === 'cro') {
391
+ const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input, funnelId: args.funnel });
392
+ out(args, formatCroSuiteOutput(r), r);
393
+ break;
394
+ }
395
+ if (args._[0] === 'backlinks') {
396
+ const r = await auditBacklinks(root, { input: args.input, target: args.target, minSeverity: args.minSeverity });
397
+ out(args, formatBacklinksOutput(r), r);
398
+ break;
399
+ }
400
+ if (args._[0] === 'edge') {
401
+ const file = args._[1];
402
+ if (!file || !args.format) throw new Error('usage: citable audit edge <file> --format <cloudflare-worker|cloudflare-cro|vercel-middleware|shopify-snippet|html>');
403
+ const content = fs.readFileSync(path.resolve(root, file), 'utf8');
404
+ const r = auditEdgeCode(content, { format: args.format, name: file });
405
+ const txt = `audit edge ${file} (${r.format}, ${r.byte_size} bytes)
406
+ Findings: ${r.finding_count} (high:${r.high} medium:${r.medium} advisory:${r.advisory})
407
+ ${r.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}\n Fix: ${f.remediation}`).join('\n') || ' no risk patterns found'}
408
+ Methodology: ${r.methodology}`;
409
+ out(args, txt, r);
410
+ break;
411
+ }
302
412
  const scope = args._[0];
303
- const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate });
413
+ const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
304
414
  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 });
305
415
  break;
306
416
  }
@@ -313,6 +423,18 @@ export async function main(argv = process.argv.slice(2), options = {}) {
313
423
  }
314
424
  case 'inspect': {
315
425
  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>');
426
+ if (args._[0] === 'eeat' || args._[0] === 'content') {
427
+ if (!args._[1]) throw new Error('usage: citable inspect eeat <page> --target <dir|url>');
428
+ const r = await inspectEeat(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
429
+ out(args, formatEeatOutput(r), r);
430
+ break;
431
+ }
432
+ if (args._[0] === 'readiness' || args._[0] === 'engines') {
433
+ if (!args._[1]) throw new Error('usage: citable inspect readiness <page> --target <dir|url>');
434
+ const r = await inspectReadiness(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
435
+ out(args, formatReadinessOutput(r), r);
436
+ break;
437
+ }
316
438
  if (args._[0] === 'serp') {
317
439
  if (!args._[1]) throw new Error('usage: citable inspect serp <page> --target <dir|url>');
318
440
  const r = await inspectSerp(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
@@ -336,11 +458,18 @@ ${richList}
336
458
  if (!args._[1]) throw new Error('usage: citable inspect cro <page> --target <dir|url>');
337
459
  const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
338
460
  const txt = `Inspect CRO ${r.url}
339
- Status: ${r.conversion_status}
461
+ Status: ${r.conversion_status} (Conversion Readiness: ${r.conversion_readiness_score ?? 'N/A'}/100)
462
+ Above-the-Fold Clarity: ${r.atf_clarity?.score ?? 'N/A'}/100 (Scent: ${r.atf_clarity?.scent_match ? 'CONGRUENT' : 'GAP'})
463
+ Trust & Credibility: ${r.trust_and_credibility?.score ?? 'N/A'}/100
464
+ Friction Surface Area: ${r.friction_surface_area}
340
465
  Declared Conversion Action: ${r.declared_conversion_action || 'none'}
341
466
  Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
342
467
  Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
343
468
  Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
469
+ Primary CTA Conspicuity: ${r.saliency?.primary_cta_conspicuity_index ?? 'N/A'} (${r.saliency?.pci_assessment ?? 'unassessed'}) — modeled heuristic index, not observed attention
470
+ Keystroke Effort: ${r.keystroke_effort?.autofill_coverage_pct}% autofill coverage (${r.keystroke_effort?.keystroke_reduction_pct}% modeled mobile friction reduction)
471
+ Payment Wallet Readiness: ${r.express_checkout_readiness?.payment_wallet_readiness?.supported ? 'SUPPORTED' : 'NOT DETECTED (CRO-021)'}
472
+ Authentication Readiness (passkeys/WebAuthn): ${r.express_checkout_readiness?.authentication_readiness?.supported ? 'DETECTED' : 'NOT DETECTED'} (informational index; authentication, not payment)
344
473
  CTAs (${r.ctas.length}):
345
474
  ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
346
475
  Forms (${r.forms.length}):
@@ -350,6 +479,20 @@ ${r.forms.map((f, i) => ` Form ${i + 1}: ${f.fieldCount} fields (autocomplete
350
479
  out(args, txt, r);
351
480
  break;
352
481
  }
482
+ if (args._[0] === 'saliency') {
483
+ if (!args._[1]) throw new Error('usage: citable inspect saliency <page> --target <dir|url>');
484
+ const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
485
+ const sal = r.saliency;
486
+ const gazeLines = (sal?.predicted_gaze_path || []).map((g) => ` ${g.order}. [${g.type.toUpperCase()}] "${g.label}" (salience: ${Math.round(g.salience_score * 100)}%)`).join('\n');
487
+ const txt = `Inspect Saliency ${r.url}
488
+ Primary CTA Conspicuity Index: ${sal?.primary_cta_conspicuity_index ?? 'N/A'} [${sal?.pci_assessment ?? 'unassessed'}]
489
+ Visual Clutter Risk: ${sal?.visual_clutter ?? 'clean'} (${sal?.competing_hero_elements_count ?? 0} high-salience hero elements)
490
+ Predicted Gaze Path (First 3 Fixations):
491
+ ${gazeLines || ' none predicted'}
492
+ SVG Attention Heatmap: generated (${sal?.elements_evaluated ?? 0} elements mapped)`;
493
+ out(args, txt, { url: r.url, saliency: sal });
494
+ break;
495
+ }
353
496
  if (args._[0] === 'aeo') {
354
497
  if (!args._[1]) throw new Error('usage: citable inspect aeo <page> --target <dir|url>');
355
498
  const r = await inspectAeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
@@ -451,8 +594,84 @@ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') ||
451
594
  out(args, txt, r);
452
595
  break;
453
596
  }
597
+ case 'preview': {
598
+ const sub = args._[0];
599
+ if (sub !== 'cro') throw new Error('usage: citable preview cro <page> [--target <dir|url>] [--export <path>]');
600
+ const page = args._[1] || 'home';
601
+ const r = await previewCroCommand(root, page, { target: args.target, export: args.export });
602
+ const txt = `Preview CRO ${r.url}
603
+ Friction Surface Area (Before): ${r.fsa_before}
604
+ Friction Surface Area (Modeled Target): ${r.fsa_after} (modeled index, not a measured outcome)
605
+ Resolved Findings: ${r.findings_count}
606
+ HTML Preview: ${r.saved_file || 'rendered in memory (use --export <file.html> to save)'}`;
607
+ out(args, txt, r);
608
+ break;
609
+ }
610
+ case 'remediate': {
611
+ const r = await remediateCommand(root, {
612
+ finding: args.finding || args._[0],
613
+ component: args.component,
614
+ target: args.target,
615
+ format: args.format || 'react',
616
+ write: args.write,
617
+ });
618
+ if (r.target) {
619
+ const parts = [`Remediate ${r.finding_id} → ${r.recommended_component}`, ` Rationale: ${r.rationale}`, ` Scaffold Command: ${r.scaffold_command}`];
620
+ parts.push(` Target File: ${r.target}`);
621
+ parts.push(` Framework: ${r.framework?.framework || 'unknown'} (detection confidence ${r.framework?.confidence ?? 0})`);
622
+ parts.push(` Patch: ${r.changed ? `${r.match_count} match(es)` : 'no match (already remediated or not applicable)'}, idempotent: ${r.idempotent}, class: ${r.patcher_class}`);
623
+ if (r.validation) {
624
+ for (const c of r.validation.checks) parts.push(` Check ${c.check_id}: ${c.passed ? 'PASS' : 'FAIL'} — ${c.detail}`);
625
+ if (r.validation.methodology) parts.push(` Methodology: ${r.validation.methodology}`);
626
+ }
627
+ if (r.confidence) {
628
+ parts.push(` Confidence: ${r.confidence.score} (write threshold ${r.write_policy?.min_confidence ?? 0.7})`);
629
+ for (const f of r.confidence.factors) parts.push(` - ${f.factor}: ${f.weight} (${f.detail})`);
630
+ }
631
+ parts.push(r.written
632
+ ? ` Written. Rollback snapshot: ${r.rollback?.snapshot_file}`
633
+ : r.write_refused
634
+ ? ` Write REFUSED (fail closed): ${r.refusal_reason}`
635
+ : ` Dry run — unified diff below. Use --write to apply.`);
636
+ if (r.diff) parts.push('Unified Diff:', r.diff.trimEnd());
637
+ parts.push('', 'Code Snippet (recommended component):', '--------------------------------------------------', r.code, '--------------------------------------------------');
638
+ out(args, parts.join('\n'), r);
639
+ break;
640
+ }
641
+ const txt = r.recommended_component
642
+ ? `Remediate ${r.finding_id} → ${r.recommended_component}
643
+ Rationale: ${r.rationale}
644
+ Scaffold Command: ${r.scaffold_command}
645
+ Code Snippet (${r.format}):
646
+ --------------------------------------------------
647
+ ${r.code}
648
+ --------------------------------------------------`
649
+ : r.component
650
+ ? `Remediate Component: ${r.component}
651
+ Scaffold Command: ${r.scaffold_command}
652
+ Code Snippet:
653
+ --------------------------------------------------
654
+ ${r.code}
655
+ --------------------------------------------------`
656
+ : `Available Components:
657
+ ${(r.available_components || []).map((c) => ` - ${c.name} (${c.id}): ${c.description}`).join('\n')}
658
+ Supported Findings: ${(r.supported_findings || []).join(', ')}`;
659
+ out(args, txt, r);
660
+ break;
661
+ }
454
662
  case 'test-funnel':
455
663
  case 'test': {
664
+ if (cmd === 'test' && args._[0] === 'visual') {
665
+ const preview = args['preview-file'] ? fs.readFileSync(path.resolve(root, args['preview-file']), 'utf8') : null;
666
+ const r = await runVisualRegression(root, { previewHtml: preview, output: args.output });
667
+ const txt = `test visual
668
+ Contract checks: ${r.contract.ok ? 'ALL PASS' : 'FAIL'} (${r.contract.checks.filter((c) => c.passed).length}/${r.contract.checks.length})
669
+ ${r.contract.checks.map((c) => ` ${c.passed ? '✔' : '✖'} ${c.check_id}: ${c.detail}`).join('\n')}
670
+ Matrix cells: ${r.matrix_cells.length}
671
+ Screenshots: ${r.screenshots.status}${r.screenshots.required_input ? ` (required_input: ${r.screenshots.required_input})` : ` (${r.screenshots.captured.length} captured)`}`;
672
+ out(args, txt, r);
673
+ break;
674
+ }
456
675
  if (cmd === 'test-funnel' || args._[0] === 'funnel') {
457
676
  const funnelId = cmd === 'test-funnel' ? args._[0] : args._[1];
458
677
  const r = await testFunnel(root, funnelId, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
@@ -510,7 +729,8 @@ ${r.issues.map((iss) => ` - ${iss}`).join('\n') || ' none'}
510
729
  if (sub === 'edge') {
511
730
  const format = args.format || 'cloudflare-redirects';
512
731
  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);
732
+ const sec = r.security;
733
+ 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);
514
734
  break;
515
735
  }
516
736
  throw new Error('usage: citable export edge [--format <cloudflare-redirects|cloudflare-waf|cloudflare-worker>]');
@@ -577,6 +797,16 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
577
797
  break;
578
798
  }
579
799
  case 'action-plan': {
800
+ if (args.matrix) {
801
+ const r = await prioritizeCommand(['matrix', ...(args._[0] ? ['--run', args._[0]] : ['--findings'])], root);
802
+ out(args, r.formatted || JSON.stringify(r, null, 2), r);
803
+ break;
804
+ }
805
+ if (args.roadmap) {
806
+ const r = await roadmapCommand(root, { runId: args._[0], target: args.target, write: args.write !== false });
807
+ out(args, r.markdown, r);
808
+ break;
809
+ }
580
810
  const r = actionPlan(root, { runId: args._[0] });
581
811
  out(args, `action-plan: ${r.summary.total_actions} action(s) [ready:${r.summary.ready} blocked:${r.summary.blocked}]\nPlan: ${path.join(r.dir, 'action-plan.md')}\nSource audit: ${r.source_run_id}`, r);
582
812
  break;
@@ -629,11 +859,46 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
629
859
  } else if (sub === 'consensus') {
630
860
  const r = reportConsensus(root, { runId: args.runId, since: args.since, last: args.last ? Number(args.last) : undefined });
631
861
  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);
862
+ } else if (sub === 'search' || sub === 'seo') {
863
+ const r = await exportExecutiveReport(root, args.runId, {
864
+ type: 'search',
865
+ format: args.format || 'markdown',
866
+ clientName: args.client,
867
+ output: args.output,
868
+ target: args.target,
869
+ baseUrl: args.baseUrl,
870
+ refDate: args.refDate,
871
+ input: args.input,
872
+ });
873
+ out(args, r.content, r.data);
874
+ } else if (sub === 'cro') {
875
+ const r = await exportExecutiveReport(root, args.runId, {
876
+ type: 'cro',
877
+ format: args.format || 'markdown',
878
+ clientName: args.client,
879
+ output: args.output,
880
+ target: args.target,
881
+ baseUrl: args.baseUrl,
882
+ refDate: args.refDate,
883
+ input: args.input,
884
+ funnelId: args.funnel,
885
+ });
886
+ out(args, r.content, r.data);
632
887
  } else if (sub === 'export') {
633
- const r = await exportExecutiveReport(root, args.runId, { format: args.format, clientName: args.client, output: args.output });
888
+ const r = await exportExecutiveReport(root, args.runId, {
889
+ format: args.format,
890
+ clientName: args.client,
891
+ output: args.output,
892
+ type: args.type,
893
+ target: args.target,
894
+ baseUrl: args.baseUrl,
895
+ refDate: args.refDate,
896
+ input: args.input,
897
+ funnelId: args.funnel,
898
+ });
634
899
  out(args, `Executive Report Exported (${r.format}) for "${r.client_name}"${r.output_path ? ` to ${r.output_path}` : ''}\n\n${r.content}`, r);
635
900
  } else {
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>]');
901
+ throw new Error('usage: citable report <dashboard|share-of-voice|consensus|search|cro|export> [--last <n>] [--since <run-id>] [--run <run-id>] [--format <md|html|json>]');
637
902
  }
638
903
  break;
639
904
  }
@@ -831,7 +1096,14 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
831
1096
  } else if (mode === 'compare-receipts') {
832
1097
  const r = readAndCompareAcceptanceReceipts(args._[1], args._[2]);
833
1098
  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);
834
- } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts> [options]');
1099
+ } else if (mode === 'benchmark') {
1100
+ const r = await runGoldenBenchmark(root, { corpusDir: args.input, writeReport: args.output });
1101
+ const txt = `corpus benchmark v${r.corpus_version}: ${r.pages_evaluated} page(s) across ${r.sites_evaluated} site(s)
1102
+ 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'})
1103
+ ${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')}
1104
+ Limitations: ${r.limitations[0]}`;
1105
+ out(args, txt, r);
1106
+ } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts|benchmark> [options]');
835
1107
  break;
836
1108
  }
837
1109
  case 'artifacts': {
@@ -848,6 +1120,93 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
848
1120
  } else throw new Error('usage: citable artifacts <export <run-id> --output <directory>|verify --input <directory>|import --input <directory>>');
849
1121
  break;
850
1122
  }
1123
+ case 'verify': {
1124
+ const mode = args._[0];
1125
+ if (mode === 'page') {
1126
+ const r = await verifyPage(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
1127
+ const sev = Object.entries(r.by_severity).filter(([, n]) => n > 0).map(([k, n]) => `${k}:${n}`).join(' ') || 'none';
1128
+ const txt = `verify page ${r.page}
1129
+ Status: ${r.status}
1130
+ Findings: ${r.finding_count} (${sev})
1131
+ Detectors run: ${r.provenance.detectors_run.length}
1132
+ Definition: ${r.provenance.definition}`;
1133
+ out(args, txt, r);
1134
+ break;
1135
+ }
1136
+ if (mode !== 'remediation') throw new Error('usage: citable verify <remediation --run <run-id> --finding <detector-id> [|page <page>] [--target <dir|file>] [--apply]');
1137
+ const r = await verifyRemediation(root, {
1138
+ run: args.runId || args._[1],
1139
+ finding: args.finding,
1140
+ target: args.target,
1141
+ apply: args.apply,
1142
+ subject: args.subject,
1143
+ baseUrl: args['base-url'],
1144
+ refDate: args['ref-date'],
1145
+ });
1146
+ const subject = r.subject?.identifier || 'unknown subject';
1147
+ const patchLine = r.patch
1148
+ ? `\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}` : ''}`
1149
+ : '';
1150
+ const txt = `verify remediation ${r.detector_id} on ${subject}
1151
+ Verdict: ${r.status}${r.verdict.resolved ? ' (finding no longer reported)' : ''}
1152
+ Before finding(s): ${r.verdict.before_finding_ids.join(', ') || 'none'}
1153
+ After finding(s): ${r.verdict.after_finding_ids.join(', ') || 'none'}
1154
+ New critical/high regressions: ${r.verdict.new_regressions.length}${patchLine}
1155
+ Re-check target: ${r.provenance.recheck_target || 'n/a'}
1156
+ Resolution criterion: detector absence for the same subject; not an outcome guarantee
1157
+ Bundle: ${r.bundle_dir || 'not written'}`;
1158
+ out(args, txt, r);
1159
+ break;
1160
+ }
1161
+ case 'kit': {
1162
+ if (args._[0] !== 'export') throw new Error('usage: citable kit export --run <run-id> --finding <detector-id> [--target <file>] [--subject <id|url>] [--output <dir>]');
1163
+ const r = await exportImplementationKit(root, {
1164
+ run: args.runId,
1165
+ finding: args.finding,
1166
+ target: args.target,
1167
+ subject: args.subject,
1168
+ output: args.output,
1169
+ format: args.format,
1170
+ });
1171
+ const txt = `kit export ${r.detector_id}
1172
+ Kit directory: ${r.kit_dir}
1173
+ Files: ${r.files.length}
1174
+ Rendering evidence files: ${r.rendering_evidence_count}
1175
+ Patch diff included: ${r.has_patch_diff}`;
1176
+ out(args, txt, r);
1177
+ break;
1178
+ }
1179
+ case 'compatibility': {
1180
+ const r = await compatibilityCommand(root, args);
1181
+ const txt = `compatibility (citable ${r.tool_version}, Node ${r.node})
1182
+ ${r.checks.map((c) => ` [${c.severity.toUpperCase()}] ${c.check_id}: ${c.detail}`).join('\n')}
1183
+ Blockers: ${r.blocker_count}
1184
+ Note: ${r.note}`;
1185
+ out(args, txt, r);
1186
+ break;
1187
+ }
1188
+ case 'check': {
1189
+ if (args._[0] === 'experiment') {
1190
+ const r = await checkExperiment(root, {
1191
+ experimentId: args._[1],
1192
+ observedControl: args['observed-control'] !== undefined ? Number(args['observed-control']) : undefined,
1193
+ observedVariant: args['observed-variant'] !== undefined ? Number(args['observed-variant']) : undefined,
1194
+ daysRunning: args['days-running'] !== undefined ? Number(args['days-running']) : undefined,
1195
+ baselineRate: args['baseline-rate'],
1196
+ mde: args.mde,
1197
+ pValue: args['p-value'],
1198
+ revenueClaimed: args['revenue-claimed'],
1199
+ });
1200
+ const txt = `check experiment ${r.experiment_id}
1201
+ Lifecycle: ${r.lifecycle} — ${r.lifecycle_definition}
1202
+ Guardrails: ${r.guardrail_count} finding(s)
1203
+ ${r.findings.map((f) => ` [${f.guardrail_id}] (${f.severity}) ${f.summary}`).join('\n') || ' no guardrail findings'}
1204
+ Note: ${r.note}`;
1205
+ out(args, txt, r);
1206
+ break;
1207
+ }
1208
+ throw new Error('usage: citable check experiment <id> [--observed-control N --observed-variant N --days-running N --p-value X --revenue-claimed]');
1209
+ }
851
1210
  case 'self-upgrade': {
852
1211
  const output = await selfUpgradeCommand(argv.slice(1));
853
1212
  console.log(output);
@@ -915,6 +1274,56 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
915
1274
  out(args, JSON.stringify(r, null, 2), r);
916
1275
  break;
917
1276
  }
1277
+ case 'cro': {
1278
+ const sub = args._[0];
1279
+ if (sub === 'backlog') {
1280
+ const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input });
1281
+ out(args, formatBacklogMarkdown(r.experiment_backlog), r.experiment_backlog);
1282
+ break;
1283
+ }
1284
+ if (sub === 'roadmap') {
1285
+ const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input });
1286
+ out(args, formatCroRoadmapMarkdown(r.strategic_roadmap), r.strategic_roadmap);
1287
+ break;
1288
+ }
1289
+ const r = await auditCroSuite(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, input: args.input, funnelId: args.funnel });
1290
+ out(args, formatCroSuiteOutput(r), r);
1291
+ break;
1292
+ }
1293
+ case 'sweep': {
1294
+ const sub = args._[0];
1295
+ if (sub && sub !== 'technical') throw new Error('usage: citable sweep technical [--target <dir|url>]');
1296
+ const r = await sweepTechnical(root, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, page: args._[1] });
1297
+ out(args, formatSweepOutput(r), r);
1298
+ break;
1299
+ }
1300
+ case 'matrix': {
1301
+ const r = await prioritizeCommand(['matrix', ...argv.slice(1)], root);
1302
+ out(args, r.formatted || JSON.stringify(r, null, 2), r);
1303
+ break;
1304
+ }
1305
+ case 'roadmap': {
1306
+ const r = await roadmapCommand(root, { runId: args.runId, target: args.target, write: args.write !== false });
1307
+ out(args, r.markdown, r);
1308
+ break;
1309
+ }
1310
+ case 'sow': {
1311
+ const r = await sowCommand(argv.slice(1), root);
1312
+ out(args, r.content || r.message || JSON.stringify(r, null, 2), r.data || r);
1313
+ break;
1314
+ }
1315
+ case 'inspect-eeat': {
1316
+ if (!args._[0]) throw new Error('usage: citable inspect-eeat <page> --target <dir|url>');
1317
+ const r = await inspectEeat(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
1318
+ out(args, formatEeatOutput(r), r);
1319
+ break;
1320
+ }
1321
+ case 'inspect-readiness': {
1322
+ if (!args._[0]) throw new Error('usage: citable inspect-readiness <page> --target <dir|url>');
1323
+ const r = await inspectReadiness(root, args._[0], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
1324
+ out(args, formatReadinessOutput(r), r);
1325
+ break;
1326
+ }
918
1327
  case undefined:
919
1328
  console.log(HELP);
920
1329
  break;
@@ -0,0 +1,48 @@
1
+ import { buildContext } from './context.js';
2
+ import { safePath } from '../detectors/framework.js';
3
+ import { evaluateAnswerEngineReadiness } from '../analysis/readiness.js';
4
+
5
+ /**
6
+ * `citable inspect readiness <page>` — evaluate page extraction and readiness across
7
+ * Perplexity, Bing Copilot, and ChatGPT / SearchGPT.
8
+ */
9
+ export async function inspectReadiness(root, pageRef, { target, baseUrl, refDate } = {}) {
10
+ const ctx = await buildContext(root, { target, baseUrl, refDate });
11
+ if (!ctx.site) throw new Error('inspect readiness requires a site target (built output directory or URL)');
12
+ const wanted = safePath(pageRef, ctx.site.baseUrl);
13
+ const page = ctx.site.pages.find((p) => safePath(p.url) === wanted || p.sourceFile === pageRef);
14
+ if (!page) throw new Error(`page not found in audited output: ${pageRef}`);
15
+
16
+ return evaluateAnswerEngineReadiness(page, ctx);
17
+ }
18
+
19
+ /**
20
+ * Format terminal output for `citable inspect readiness <page>`
21
+ */
22
+ export function formatReadinessOutput(r) {
23
+ const lines = [
24
+ `Answer-Engine Readiness: ${r.url}`,
25
+ `==============================================`,
26
+ `Composite Readiness Score: ${r.composite_readiness_score} / 100`,
27
+ ``,
28
+ `ENGINE EVALUATIONS:`,
29
+ ` Perplexity: ${r.engines.perplexity.score}/100 [${r.engines.perplexity.status.toUpperCase()}] (Bot: ${r.engines.perplexity.primary_bot})`,
30
+ ` Bing Copilot: ${r.engines.bing_copilot.score}/100 [${r.engines.bing_copilot.status.toUpperCase()}] (Bot: ${r.engines.bing_copilot.primary_bot})`,
31
+ ` ChatGPT: ${r.engines.chatgpt.score}/100 [${r.engines.chatgpt.status.toUpperCase()}] (Bot: ${r.engines.chatgpt.primary_bot})`,
32
+ ``,
33
+ `CROSS-ENGINE CAPABILITY MATRIX:`,
34
+ ];
35
+
36
+ for (const row of r.comparison_matrix) {
37
+ lines.push(` - ${row.capability.padEnd(28)} | Perplexity: ${row.perplexity.padEnd(10)} | Copilot: ${row.bing_copilot.padEnd(14)} | ChatGPT: ${row.chatgpt}`);
38
+ }
39
+
40
+ if (r.recommendations.length) {
41
+ lines.push(``, `PRIORITIZED ACTIONS:`);
42
+ for (const rec of r.recommendations) {
43
+ lines.push(` [${rec.priority.toUpperCase()}] (${rec.engine.toUpperCase()}) ${rec.action}`);
44
+ }
45
+ }
46
+
47
+ return lines.join('\n');
48
+ }
@@ -12,8 +12,8 @@ import { buildEntityGraph } from '../observations/entityGraph.js';
12
12
  import { buildSourceIdentityChain } from '../observations/sourceIdentity.js';
13
13
 
14
14
  /** `citable audit [scope]` — run detectors and produce an evidence package. */
15
- export async function audit(root, { target, scope, baseUrl, refDate } = {}) {
16
- const ctx = await buildContext(root, { target, baseUrl, refDate });
15
+ export async function audit(root, { target, scope, baseUrl, refDate, viewport = null } = {}) {
16
+ const ctx = await buildContext(root, { target, baseUrl, refDate, viewport });
17
17
  const detectors = selectDetectors({ scope });
18
18
 
19
19
  const run = createRun(root, {