@nebulacomponents/citable 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +16 -3
  3. package/dist/universal/.agents/skills/citable/SKILL.md +47 -1
  4. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  5. package/dist/universal/.agents/skills/citable/manifest.json +8 -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/.claude/agents/citable/manifest.json +2 -2
  10. package/dist/universal/.claude/skills/citable/SKILL.md +47 -1
  11. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  12. package/dist/universal/.claude/skills/citable/manifest.json +8 -6
  13. package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  14. package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
  15. package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
  16. package/dist/universal/.cursor/skills/citable/SKILL.md +47 -1
  17. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  18. package/dist/universal/.cursor/skills/citable/manifest.json +8 -6
  19. package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  20. package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
  21. package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
  22. package/dist/universal/.gemini/skills/citable/SKILL.md +47 -1
  23. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  24. package/dist/universal/.gemini/skills/citable/manifest.json +8 -6
  25. package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  26. package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
  27. package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
  28. package/dist/universal/.github/skills/citable/SKILL.md +47 -1
  29. package/dist/universal/.github/skills/citable/VERSION +1 -1
  30. package/dist/universal/.github/skills/citable/manifest.json +8 -6
  31. package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  32. package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
  33. package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
  34. package/dist/universal/.kiro/skills/citable/SKILL.md +47 -1
  35. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  36. package/dist/universal/.kiro/skills/citable/manifest.json +8 -6
  37. package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  38. package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
  39. package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
  40. package/dist/universal/.opencode/skills/citable/SKILL.md +47 -1
  41. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  42. package/dist/universal/.opencode/skills/citable/manifest.json +8 -6
  43. package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  44. package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
  45. package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
  46. package/dist/universal/.pi/agent/skills/citable/SKILL.md +47 -1
  47. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  48. package/dist/universal/.pi/agent/skills/citable/manifest.json +8 -6
  49. package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  50. package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
  51. package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
  52. package/dist/universal/.qoder/skills/citable/SKILL.md +47 -1
  53. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  54. package/dist/universal/.qoder/skills/citable/manifest.json +8 -6
  55. package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  56. package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
  57. package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
  58. package/dist/universal/.rovodev/skills/citable/SKILL.md +47 -1
  59. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  60. package/dist/universal/.rovodev/skills/citable/manifest.json +8 -6
  61. package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  62. package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
  63. package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
  64. package/dist/universal/.trae/skills/citable/SKILL.md +47 -1
  65. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  66. package/dist/universal/.trae/skills/citable/manifest.json +8 -6
  67. package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  68. package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
  69. package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
  70. package/dist/universal/.trae-cn/skills/citable/SKILL.md +47 -1
  71. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  72. package/dist/universal/.trae-cn/skills/citable/manifest.json +8 -6
  73. package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
  74. package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
  75. package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
  76. package/dist/universal/manifest.json +100 -76
  77. package/package.json +1 -1
  78. package/schemas/cli-output-envelope.schema.json +30 -0
  79. package/schemas/finding.schema.json +69 -0
  80. package/schemas/remediation-verification.schema.json +98 -0
  81. package/skill/SKILL.md +47 -1
  82. package/src/analysis/saliency.js +119 -0
  83. package/src/cli/index.js +269 -6
  84. package/src/commands/audit.js +2 -2
  85. package/src/commands/ciWorkflow.js +63 -0
  86. package/src/commands/compatibility.js +205 -0
  87. package/src/commands/context.js +2 -1
  88. package/src/commands/edgeRules.js +80 -4
  89. package/src/commands/edgeSecurity.js +192 -0
  90. package/src/commands/experimentGuardrails.js +213 -0
  91. package/src/commands/goldenCorpus.js +155 -0
  92. package/src/commands/implementationKit.js +202 -0
  93. package/src/commands/inspectCro.js +60 -0
  94. package/src/commands/planExperiment.js +52 -0
  95. package/src/commands/previewCro.js +241 -0
  96. package/src/commands/remediate.js +461 -0
  97. package/src/commands/verifyRemediation.js +204 -0
  98. package/src/commands/visualRegression.js +165 -0
  99. package/src/components/index.js +556 -0
  100. package/src/detectors/cro.js +48 -0
  101. package/src/detectors/framework.js +31 -1
  102. package/src/reporting/executiveExport.js +113 -1
package/src/cli/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import { init } from '../commands/init.js';
5
6
  import { audit } from '../commands/audit.js';
7
+ import { auditEdgeCode } from '../commands/edgeSecurity.js';
6
8
  import { planAudit } from '../commands/planAudit.js';
7
9
  import { demo } from '../commands/demo.js';
8
10
  import { validate } from '../commands/validate.js';
@@ -21,6 +23,14 @@ import { probeEngine } from '../commands/probe.js';
21
23
  import { lintComponents } from '../commands/lintComponents.js';
22
24
  import { exportExecutiveReport } from '../reporting/executiveExport.js';
23
25
  import { attributeImpact } from '../commands/attributeImpact.js';
26
+ import { remediateCommand } from '../commands/remediate.js';
27
+ import { verifyRemediation } from '../commands/verifyRemediation.js';
28
+ import { exportImplementationKit } from '../commands/implementationKit.js';
29
+ import { compatibilityCommand, verifyPage } from '../commands/compatibility.js';
30
+ import { runVisualRegression } from '../commands/visualRegression.js';
31
+ import { checkExperiment } from '../commands/experimentGuardrails.js';
32
+ import { runGoldenBenchmark } from '../commands/goldenCorpus.js';
33
+ import { previewCroCommand } from '../commands/previewCro.js';
24
34
  import { schemaCommand } from '../commands/schemaCmd.js';
25
35
  import { compareSnapshots } from '../commands/compareSnapshots.js';
26
36
  import { isInstallerCommand, runInstallerCommand } from '../installer/index.js';
@@ -76,10 +86,15 @@ Commands
76
86
  inspect <page> Profile one page (URL path or source file)
77
87
  inspect serp <page> Simulate Google SERP title/snippet truncation and rich results
78
88
  inspect cro <page> Evaluate page conversion readiness, CTA visibility, and friction
89
+ inspect saliency <page> Evaluate privacy-first algorithmic visual attention and CTA conspicuity
79
90
  inspect aeo <page> Evaluate answer extractability, question density, and structure
80
91
  inspect geo <page> Evaluate RAG chunkability, section density, and retrieval posture
92
+ preview cro <page> Interactive split-screen visual preview of original vs remediated page
93
+ remediate [finding] Synthesize accessible Nebula Components; safe patches with diff, validation, confidence, rollback (--write gated, fail closed)
81
94
  test funnel [id] Test multi-step conversion funnel continuity and parameter persistence
95
+ test visual Deterministic layout-contract checks + viewport/variant screenshot matrix (screenshots need playwright)
82
96
  plan experiment Calculate statistical sample size and duration for A/B testing
97
+ check experiment Experiment guardrails: SRM, stopping, power, contamination, lifecycle status
83
98
  map-claims Extract material claim candidates from pages (--write to save)
84
99
  substantiate Assess claim/evidence status (--write to apply downgrades)
85
100
  schema Validate deployed JSON-LD and propose registry-derived schema
@@ -123,9 +138,14 @@ Commands
123
138
  corpus evaluate Evaluate a disclosed real-property acceptance corpus
124
139
  corpus publish Validate and project an owner-authorized public corpus
125
140
  corpus receipt Create a reproducibility receipt for a sealed run
141
+ corpus benchmark Run the labeled golden fixture corpus and report per-detector precision/recall
126
142
  corpus compare-receipts Compare two acceptance-run receipt envelopes
127
143
  artifacts export Export one sealed run as a portable verified directory
128
144
  artifacts verify Verify an exported artifact interchange directory
145
+ verify remediation Closed loop: re-run the detector after a patch and emit a before/after evidence bundle
146
+ verify page <page> Run all detectors scoped to one page and report posture (pass/attention/blocked)
147
+ compatibility Pre-flight: Node engine, optional adapters, browser, framework, registries, edge limits
148
+ kit export Customer-ready implementation kit: finding, diff, evidence, acceptance tests, deployment
129
149
  artifacts import Import a verified run without changing its canonical bytes
130
150
  self-upgrade Check for a newer version and upgrade the npx cache
131
151
  kpi [list|show|validate] KPI architecture — govern metric definitions, sources, targets
@@ -203,6 +223,7 @@ function parseArgs(argv) {
203
223
  else if (a === '--target') args.target = argv[++i];
204
224
  else if (a === '--base-url') args.baseUrl = argv[++i];
205
225
  else if (a === '--ref-date') args.refDate = argv[++i];
226
+ else if (a === '--viewport') args.viewport = argv[++i];
206
227
  else if (a === '--input') args.input = argv[++i];
207
228
  else if (a === '--output') args.output = argv[++i];
208
229
  else if (a === '--run') args.runId = argv[++i];
@@ -254,14 +275,53 @@ function parseArgs(argv) {
254
275
  else if (a === '--note') args.note = argv[++i];
255
276
  else if (a === '--expired') args.expiredOnly = true;
256
277
  else if (a === '--expiring-soon') args.expiringSoonDays = Number(argv[++i]);
278
+ else if (a === '--finding') args.finding = argv[++i];
279
+ else if (a === '--component') args.component = argv[++i];
280
+ else if (a === '--format') args.format = argv[++i];
281
+ else if (a === '--strict') args.strict = true;
282
+ else if (a === '--export') args.export = argv[++i];
283
+ else if (a === '--apply') args.apply = true;
284
+ else if (a === '--preview-file') args['preview-file'] = argv[++i];
285
+ else if (a === '--observed-control') args['observed-control'] = argv[++i];
286
+ else if (a === '--observed-variant') args['observed-variant'] = argv[++i];
287
+ else if (a === '--days-running') args['days-running'] = argv[++i];
288
+ else if (a === '--baseline-rate') args['baseline-rate'] = argv[++i];
289
+ else if (a === '--p-value') args['p-value'] = argv[++i];
290
+ else if (a === '--revenue-claimed') args['revenue-claimed'] = true;
257
291
  else args._.push(a);
258
292
  }
259
293
  return args;
260
294
  }
261
295
 
262
- function out(args, human, data) {
263
- if (args.json) console.log(JSON.stringify(data, null, 2));
264
- else console.log(human);
296
+ const TOOL_VERSION_CLI = JSON.parse(
297
+ fs.readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
298
+ ).version;
299
+
300
+ // Set by main(); read by out() for the JSON envelope. Kept off `args` because
301
+ // several commands spread parsed args directly into option objects.
302
+ let CURRENT_COMMAND = null;
303
+
304
+ /**
305
+ * Stable machine-readable output contract (citable_output_schema 1.0):
306
+ * every command's --json payload is wrapped in this envelope so CI systems
307
+ * and agencies can consume results without parsing terminal text. The
308
+ * envelope version changes only on breaking envelope changes; command
309
+ * payloads evolve backward-compatibly inside `result`.
310
+ */
311
+ const OUTPUT_SCHEMA_VERSION = '1.0';
312
+
313
+ function out(args, human, data, command = null) {
314
+ if (args.json) {
315
+ console.log(JSON.stringify({
316
+ citable_output_schema: OUTPUT_SCHEMA_VERSION,
317
+ tool_version: TOOL_VERSION_CLI,
318
+ command: command ?? CURRENT_COMMAND,
319
+ generated_at: new Date().toISOString(),
320
+ result: data,
321
+ }, null, 2));
322
+ } else {
323
+ console.log(human);
324
+ }
265
325
  }
266
326
 
267
327
  export async function main(argv = process.argv.slice(2), options = {}) {
@@ -272,6 +332,7 @@ export async function main(argv = process.argv.slice(2), options = {}) {
272
332
  }
273
333
  if (isInstallerCommand(cmd)) return runInstallerCommand(cmd, argv.slice(1), options);
274
334
  const args = parseArgs(argv.slice(1));
335
+ CURRENT_COMMAND = cmd;
275
336
  const root = options.cwd ?? process.cwd();
276
337
 
277
338
  try {
@@ -299,8 +360,20 @@ export async function main(argv = process.argv.slice(2), options = {}) {
299
360
  break;
300
361
  }
301
362
  case 'audit': {
363
+ if (args._[0] === 'edge') {
364
+ const file = args._[1];
365
+ if (!file || !args.format) throw new Error('usage: citable audit edge <file> --format <cloudflare-worker|cloudflare-cro|vercel-middleware|shopify-snippet|html>');
366
+ const content = fs.readFileSync(path.resolve(root, file), 'utf8');
367
+ const r = auditEdgeCode(content, { format: args.format, name: file });
368
+ const txt = `audit edge ${file} (${r.format}, ${r.byte_size} bytes)
369
+ Findings: ${r.finding_count} (high:${r.high} medium:${r.medium} advisory:${r.advisory})
370
+ ${r.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}\n Fix: ${f.remediation}`).join('\n') || ' no risk patterns found'}
371
+ Methodology: ${r.methodology}`;
372
+ out(args, txt, r);
373
+ break;
374
+ }
302
375
  const scope = args._[0];
303
- const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate });
376
+ const r = await audit(root, { target: args.target, scope, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
304
377
  out(args, `Audit ${r.runId}: ${r.summary.total} finding(s) [${Object.entries(r.summary.by_severity).map(([k, v]) => `${k}:${v}`).join(' ')}]\nEvidence package: ${r.dir}\nReport: ${path.join(r.dir, 'report.md')}\nStatus: ${r.manifest.status}${r.manifest.incomplete_checks.length ? `\nIncomplete: ${r.manifest.incomplete_checks.join('; ')}` : ''}`, { runId: r.runId, dir: r.dir, summary: r.summary, status: r.manifest.status });
305
378
  break;
306
379
  }
@@ -337,10 +410,15 @@ ${richList}
337
410
  const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
338
411
  const txt = `Inspect CRO ${r.url}
339
412
  Status: ${r.conversion_status}
413
+ Friction Surface Area: ${r.friction_surface_area}
340
414
  Declared Conversion Action: ${r.declared_conversion_action || 'none'}
341
415
  Page Type: ${r.page_type || 'unregistered'}; Intent: ${r.primary_intent || 'unregistered'}
342
416
  Analytics Instrumentation: ${r.analytics_installed ? `INSTALLED (${r.analytics_tags.length} tag${r.analytics_tags.length === 1 ? '' : 's'})` : 'NOT DETECTED'}
343
417
  Hero CTAs: ${r.hero_cta_count}; Choice Overload: ${r.has_choice_overload ? 'YES (paralysis risk)' : 'no'}
418
+ Primary CTA Conspicuity: ${r.saliency?.primary_cta_conspicuity_index ?? 'N/A'} (${r.saliency?.pci_assessment ?? 'unassessed'}) — modeled heuristic index, not observed attention
419
+ Keystroke Effort: ${r.keystroke_effort?.autofill_coverage_pct}% autofill coverage (${r.keystroke_effort?.keystroke_reduction_pct}% modeled mobile friction reduction)
420
+ Payment Wallet Readiness: ${r.express_checkout_readiness?.payment_wallet_readiness?.supported ? 'SUPPORTED' : 'NOT DETECTED (CRO-021)'}
421
+ Authentication Readiness (passkeys/WebAuthn): ${r.express_checkout_readiness?.authentication_readiness?.supported ? 'DETECTED' : 'NOT DETECTED'} (informational index; authentication, not payment)
344
422
  CTAs (${r.ctas.length}):
345
423
  ${r.ctas.map((c) => ` [${c.isPrimary ? 'PRIMARY' : 'SECONDARY'}${c.inHero ? ' - HERO' : ''}] "${c.text}" (${c.tag}) → ${c.target || 'no target'}`).join('\n') || ' none'}
346
424
  Forms (${r.forms.length}):
@@ -350,6 +428,20 @@ ${r.forms.map((f, i) => ` Form ${i + 1}: ${f.fieldCount} fields (autocomplete
350
428
  out(args, txt, r);
351
429
  break;
352
430
  }
431
+ if (args._[0] === 'saliency') {
432
+ if (!args._[1]) throw new Error('usage: citable inspect saliency <page> --target <dir|url>');
433
+ const r = await inspectCro(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
434
+ const sal = r.saliency;
435
+ const gazeLines = (sal?.predicted_gaze_path || []).map((g) => ` ${g.order}. [${g.type.toUpperCase()}] "${g.label}" (salience: ${Math.round(g.salience_score * 100)}%)`).join('\n');
436
+ const txt = `Inspect Saliency ${r.url}
437
+ Primary CTA Conspicuity Index: ${sal?.primary_cta_conspicuity_index ?? 'N/A'} [${sal?.pci_assessment ?? 'unassessed'}]
438
+ Visual Clutter Risk: ${sal?.visual_clutter ?? 'clean'} (${sal?.competing_hero_elements_count ?? 0} high-salience hero elements)
439
+ Predicted Gaze Path (First 3 Fixations):
440
+ ${gazeLines || ' none predicted'}
441
+ SVG Attention Heatmap: generated (${sal?.elements_evaluated ?? 0} elements mapped)`;
442
+ out(args, txt, { url: r.url, saliency: sal });
443
+ break;
444
+ }
353
445
  if (args._[0] === 'aeo') {
354
446
  if (!args._[1]) throw new Error('usage: citable inspect aeo <page> --target <dir|url>');
355
447
  const r = await inspectAeo(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
@@ -451,8 +543,84 @@ ${r.headings.questions.map((q) => ` [H${q.level}] "${q.text}"`).join('\n') ||
451
543
  out(args, txt, r);
452
544
  break;
453
545
  }
546
+ case 'preview': {
547
+ const sub = args._[0];
548
+ if (sub !== 'cro') throw new Error('usage: citable preview cro <page> [--target <dir|url>] [--export <path>]');
549
+ const page = args._[1] || 'home';
550
+ const r = await previewCroCommand(root, page, { target: args.target, export: args.export });
551
+ const txt = `Preview CRO ${r.url}
552
+ Friction Surface Area (Before): ${r.fsa_before}
553
+ Friction Surface Area (Modeled Target): ${r.fsa_after} (modeled index, not a measured outcome)
554
+ Resolved Findings: ${r.findings_count}
555
+ HTML Preview: ${r.saved_file || 'rendered in memory (use --export <file.html> to save)'}`;
556
+ out(args, txt, r);
557
+ break;
558
+ }
559
+ case 'remediate': {
560
+ const r = await remediateCommand(root, {
561
+ finding: args.finding || args._[0],
562
+ component: args.component,
563
+ target: args.target,
564
+ format: args.format || 'react',
565
+ write: args.write,
566
+ });
567
+ if (r.target) {
568
+ const parts = [`Remediate ${r.finding_id} → ${r.recommended_component}`, ` Rationale: ${r.rationale}`, ` Scaffold Command: ${r.scaffold_command}`];
569
+ parts.push(` Target File: ${r.target}`);
570
+ parts.push(` Framework: ${r.framework?.framework || 'unknown'} (detection confidence ${r.framework?.confidence ?? 0})`);
571
+ parts.push(` Patch: ${r.changed ? `${r.match_count} match(es)` : 'no match (already remediated or not applicable)'}, idempotent: ${r.idempotent}, class: ${r.patcher_class}`);
572
+ if (r.validation) {
573
+ for (const c of r.validation.checks) parts.push(` Check ${c.check_id}: ${c.passed ? 'PASS' : 'FAIL'} — ${c.detail}`);
574
+ if (r.validation.methodology) parts.push(` Methodology: ${r.validation.methodology}`);
575
+ }
576
+ if (r.confidence) {
577
+ parts.push(` Confidence: ${r.confidence.score} (write threshold ${r.write_policy?.min_confidence ?? 0.7})`);
578
+ for (const f of r.confidence.factors) parts.push(` - ${f.factor}: ${f.weight} (${f.detail})`);
579
+ }
580
+ parts.push(r.written
581
+ ? ` Written. Rollback snapshot: ${r.rollback?.snapshot_file}`
582
+ : r.write_refused
583
+ ? ` Write REFUSED (fail closed): ${r.refusal_reason}`
584
+ : ` Dry run — unified diff below. Use --write to apply.`);
585
+ if (r.diff) parts.push('Unified Diff:', r.diff.trimEnd());
586
+ parts.push('', 'Code Snippet (recommended component):', '--------------------------------------------------', r.code, '--------------------------------------------------');
587
+ out(args, parts.join('\n'), r);
588
+ break;
589
+ }
590
+ const txt = r.recommended_component
591
+ ? `Remediate ${r.finding_id} → ${r.recommended_component}
592
+ Rationale: ${r.rationale}
593
+ Scaffold Command: ${r.scaffold_command}
594
+ Code Snippet (${r.format}):
595
+ --------------------------------------------------
596
+ ${r.code}
597
+ --------------------------------------------------`
598
+ : r.component
599
+ ? `Remediate Component: ${r.component}
600
+ Scaffold Command: ${r.scaffold_command}
601
+ Code Snippet:
602
+ --------------------------------------------------
603
+ ${r.code}
604
+ --------------------------------------------------`
605
+ : `Available Components:
606
+ ${(r.available_components || []).map((c) => ` - ${c.name} (${c.id}): ${c.description}`).join('\n')}
607
+ Supported Findings: ${(r.supported_findings || []).join(', ')}`;
608
+ out(args, txt, r);
609
+ break;
610
+ }
454
611
  case 'test-funnel':
455
612
  case 'test': {
613
+ if (cmd === 'test' && args._[0] === 'visual') {
614
+ const preview = args['preview-file'] ? fs.readFileSync(path.resolve(root, args['preview-file']), 'utf8') : null;
615
+ const r = await runVisualRegression(root, { previewHtml: preview, output: args.output });
616
+ const txt = `test visual
617
+ Contract checks: ${r.contract.ok ? 'ALL PASS' : 'FAIL'} (${r.contract.checks.filter((c) => c.passed).length}/${r.contract.checks.length})
618
+ ${r.contract.checks.map((c) => ` ${c.passed ? '✔' : '✖'} ${c.check_id}: ${c.detail}`).join('\n')}
619
+ Matrix cells: ${r.matrix_cells.length}
620
+ Screenshots: ${r.screenshots.status}${r.screenshots.required_input ? ` (required_input: ${r.screenshots.required_input})` : ` (${r.screenshots.captured.length} captured)`}`;
621
+ out(args, txt, r);
622
+ break;
623
+ }
456
624
  if (cmd === 'test-funnel' || args._[0] === 'funnel') {
457
625
  const funnelId = cmd === 'test-funnel' ? args._[0] : args._[1];
458
626
  const r = await testFunnel(root, funnelId, { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate });
@@ -510,7 +678,8 @@ ${r.issues.map((iss) => ` - ${iss}`).join('\n') || ' none'}
510
678
  if (sub === 'edge') {
511
679
  const format = args.format || 'cloudflare-redirects';
512
680
  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);
681
+ const sec = r.security;
682
+ out(args, `Exported ${r.rules_count} edge rule(s) in format "${r.format}"${r.output_path ? ` to ${r.output_path}` : ''}\nSecurity audit: ${sec.high} high, ${sec.medium} medium, ${sec.advisory} advisory finding(s)\n${sec.findings.map((f) => ` [${f.rule_id}] (${f.severity}) ${f.summary}`).join('\n') || ' no risk patterns found'}\n${r.content}`, r);
514
683
  break;
515
684
  }
516
685
  throw new Error('usage: citable export edge [--format <cloudflare-redirects|cloudflare-waf|cloudflare-worker>]');
@@ -831,7 +1000,14 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
831
1000
  } else if (mode === 'compare-receipts') {
832
1001
  const r = readAndCompareAcceptanceReceipts(args._[1], args._[2]);
833
1002
  out(args, `corpus compare-receipts ${r.receipt_a} → ${r.receipt_b}\nComparable envelope: ${r.comparable}\nFingerprint equal: ${r.fingerprint_equal}\nPartial runs: ${r.partial_runs.length}`, r);
834
- } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts> [options]');
1003
+ } else if (mode === 'benchmark') {
1004
+ const r = await runGoldenBenchmark(root, { corpusDir: args.input, writeReport: args.output });
1005
+ const txt = `corpus benchmark v${r.corpus_version}: ${r.pages_evaluated} page(s) across ${r.sites_evaluated} site(s)
1006
+ Gate: ${r.gate.ok ? 'PASS' : 'FAIL'} (violations: ${r.gate.violation_count}, recall failures: ${r.gate.recall_failures.join(', ') || 'none'}, precision failures: ${r.gate.precision_failures.join(', ') || 'none'})
1007
+ ${r.per_detector.filter((d) => d.true_positives + d.false_negatives + d.false_positives > 0).map((d) => ` ${d.detector_id}: TP ${d.true_positives}, FP ${d.false_positives}, FN ${d.false_negatives}, P ${d.precision ?? 'n/a'}, R ${d.recall ?? 'n/a'}`).join('\n')}
1008
+ Limitations: ${r.limitations[0]}`;
1009
+ out(args, txt, r);
1010
+ } else throw new Error('usage: citable corpus <evaluate|publish|receipt|compare-receipts|benchmark> [options]');
835
1011
  break;
836
1012
  }
837
1013
  case 'artifacts': {
@@ -848,6 +1024,93 @@ ${r.recommendations.map((rec) => ` - ${rec}`).join('\n')}`;
848
1024
  } else throw new Error('usage: citable artifacts <export <run-id> --output <directory>|verify --input <directory>|import --input <directory>>');
849
1025
  break;
850
1026
  }
1027
+ case 'verify': {
1028
+ const mode = args._[0];
1029
+ if (mode === 'page') {
1030
+ const r = await verifyPage(root, args._[1], { target: args.target, baseUrl: args.baseUrl, refDate: args.refDate, viewport: args.viewport });
1031
+ const sev = Object.entries(r.by_severity).filter(([, n]) => n > 0).map(([k, n]) => `${k}:${n}`).join(' ') || 'none';
1032
+ const txt = `verify page ${r.page}
1033
+ Status: ${r.status}
1034
+ Findings: ${r.finding_count} (${sev})
1035
+ Detectors run: ${r.provenance.detectors_run.length}
1036
+ Definition: ${r.provenance.definition}`;
1037
+ out(args, txt, r);
1038
+ break;
1039
+ }
1040
+ if (mode !== 'remediation') throw new Error('usage: citable verify <remediation --run <run-id> --finding <detector-id> [|page <page>] [--target <dir|file>] [--apply]');
1041
+ const r = await verifyRemediation(root, {
1042
+ run: args.runId || args._[1],
1043
+ finding: args.finding,
1044
+ target: args.target,
1045
+ apply: args.apply,
1046
+ subject: args.subject,
1047
+ baseUrl: args['base-url'],
1048
+ refDate: args['ref-date'],
1049
+ });
1050
+ const subject = r.subject?.identifier || 'unknown subject';
1051
+ const patchLine = r.patch
1052
+ ? `\n Patch: applied=${r.patch.applied}${r.patch.refusal_reason ? ` (refused: ${r.patch.refusal_reason})` : ''}${r.patch.rollback_snapshot ? `\n Rollback snapshot: ${r.patch.rollback_snapshot}` : ''}`
1053
+ : '';
1054
+ const txt = `verify remediation ${r.detector_id} on ${subject}
1055
+ Verdict: ${r.status}${r.verdict.resolved ? ' (finding no longer reported)' : ''}
1056
+ Before finding(s): ${r.verdict.before_finding_ids.join(', ') || 'none'}
1057
+ After finding(s): ${r.verdict.after_finding_ids.join(', ') || 'none'}
1058
+ New critical/high regressions: ${r.verdict.new_regressions.length}${patchLine}
1059
+ Re-check target: ${r.provenance.recheck_target || 'n/a'}
1060
+ Resolution criterion: detector absence for the same subject; not an outcome guarantee
1061
+ Bundle: ${r.bundle_dir || 'not written'}`;
1062
+ out(args, txt, r);
1063
+ break;
1064
+ }
1065
+ case 'kit': {
1066
+ if (args._[0] !== 'export') throw new Error('usage: citable kit export --run <run-id> --finding <detector-id> [--target <file>] [--subject <id|url>] [--output <dir>]');
1067
+ const r = await exportImplementationKit(root, {
1068
+ run: args.runId,
1069
+ finding: args.finding,
1070
+ target: args.target,
1071
+ subject: args.subject,
1072
+ output: args.output,
1073
+ format: args.format,
1074
+ });
1075
+ const txt = `kit export ${r.detector_id}
1076
+ Kit directory: ${r.kit_dir}
1077
+ Files: ${r.files.length}
1078
+ Rendering evidence files: ${r.rendering_evidence_count}
1079
+ Patch diff included: ${r.has_patch_diff}`;
1080
+ out(args, txt, r);
1081
+ break;
1082
+ }
1083
+ case 'compatibility': {
1084
+ const r = await compatibilityCommand(root, args);
1085
+ const txt = `compatibility (citable ${r.tool_version}, Node ${r.node})
1086
+ ${r.checks.map((c) => ` [${c.severity.toUpperCase()}] ${c.check_id}: ${c.detail}`).join('\n')}
1087
+ Blockers: ${r.blocker_count}
1088
+ Note: ${r.note}`;
1089
+ out(args, txt, r);
1090
+ break;
1091
+ }
1092
+ case 'check': {
1093
+ if (args._[0] === 'experiment') {
1094
+ const r = await checkExperiment(root, {
1095
+ experimentId: args._[1],
1096
+ observedControl: args['observed-control'] !== undefined ? Number(args['observed-control']) : undefined,
1097
+ observedVariant: args['observed-variant'] !== undefined ? Number(args['observed-variant']) : undefined,
1098
+ daysRunning: args['days-running'] !== undefined ? Number(args['days-running']) : undefined,
1099
+ baselineRate: args['baseline-rate'],
1100
+ mde: args.mde,
1101
+ pValue: args['p-value'],
1102
+ revenueClaimed: args['revenue-claimed'],
1103
+ });
1104
+ const txt = `check experiment ${r.experiment_id}
1105
+ Lifecycle: ${r.lifecycle} — ${r.lifecycle_definition}
1106
+ Guardrails: ${r.guardrail_count} finding(s)
1107
+ ${r.findings.map((f) => ` [${f.guardrail_id}] (${f.severity}) ${f.summary}`).join('\n') || ' no guardrail findings'}
1108
+ Note: ${r.note}`;
1109
+ out(args, txt, r);
1110
+ break;
1111
+ }
1112
+ throw new Error('usage: citable check experiment <id> [--observed-control N --observed-variant N --days-running N --p-value X --revenue-claimed]');
1113
+ }
851
1114
  case 'self-upgrade': {
852
1115
  const output = await selfUpgradeCommand(argv.slice(1));
853
1116
  console.log(output);
@@ -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, {
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export function generateCiWorkflow() {
5
+ return `name: Citable CRO Funnel Sentinel
6
+ on:
7
+ pull_request:
8
+ branches: [main, master]
9
+ push:
10
+ branches: [main, master]
11
+
12
+ jobs:
13
+ cro-audit:
14
+ name: CRO Friction & Funnel Drift Sentinel
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - name: Checkout Code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 24
24
+
25
+ - name: Install Dependencies
26
+ run: npm ci
27
+
28
+ - name: Run Component Static Linter
29
+ run: npx @nebulacomponents/citable lint components src/
30
+
31
+ - name: Run Strict Shift-Left CRO Audit Gate
32
+ run: npx @nebulacomponents/citable audit cro --strict
33
+ `;
34
+ }
35
+
36
+ export function formatPrReviewComment(findings = []) {
37
+ if (findings.length === 0) {
38
+ return '### ✅ Citable CRO Sentinel: All conversion funnels and components verified clean with zero detected friction.';
39
+ }
40
+
41
+ const lines = [
42
+ '### ⚠️ Citable CRO Sentinel: Friction Detected in Pull Request',
43
+ '',
44
+ `Found **${findings.length}** conversion action defect(s) violating UX or funnel continuity criteria:`,
45
+ '',
46
+ ];
47
+
48
+ for (const f of findings) {
49
+ lines.push(`- **[${f.detector_id || 'CRO'}]** (${f.severity || 'medium'}): ${f.summary || f.observation?.summary}`);
50
+ if (f.remediation) {
51
+ lines.push(` - *Remediation*: ${f.remediation}`);
52
+ }
53
+ }
54
+
55
+ lines.push('');
56
+ lines.push('#### Recommended 1-Click Remediation');
57
+ lines.push('Run the following locally to patch components automatically with accessible Nebula Components:');
58
+ lines.push('```bash');
59
+ lines.push('npx @nebulacomponents/citable remediate --finding CRO-007 --write');
60
+ lines.push('```');
61
+
62
+ return lines.join('\n');
63
+ }