@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.
- package/CHANGELOG.md +25 -0
- package/README.md +16 -3
- package/dist/universal/.agents/skills/citable/SKILL.md +47 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +8 -6
- package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +47 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +8 -6
- package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +47 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +8 -6
- package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +47 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +8 -6
- package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.github/skills/citable/SKILL.md +47 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +8 -6
- package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +47 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +8 -6
- package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +47 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +8 -6
- package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +47 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +8 -6
- package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +47 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +8 -6
- package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +47 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +8 -6
- package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +47 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +8 -6
- package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +47 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +8 -6
- package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/manifest.json +100 -76
- package/package.json +1 -1
- package/schemas/cli-output-envelope.schema.json +30 -0
- package/schemas/finding.schema.json +69 -0
- package/schemas/remediation-verification.schema.json +98 -0
- package/skill/SKILL.md +47 -1
- package/src/analysis/saliency.js +119 -0
- package/src/cli/index.js +269 -6
- package/src/commands/audit.js +2 -2
- package/src/commands/ciWorkflow.js +63 -0
- package/src/commands/compatibility.js +205 -0
- package/src/commands/context.js +2 -1
- package/src/commands/edgeRules.js +80 -4
- package/src/commands/edgeSecurity.js +192 -0
- package/src/commands/experimentGuardrails.js +213 -0
- package/src/commands/goldenCorpus.js +155 -0
- package/src/commands/implementationKit.js +202 -0
- package/src/commands/inspectCro.js +60 -0
- package/src/commands/planExperiment.js +52 -0
- package/src/commands/previewCro.js +241 -0
- package/src/commands/remediate.js +461 -0
- package/src/commands/verifyRemediation.js +204 -0
- package/src/commands/visualRegression.js +165 -0
- package/src/components/index.js +556 -0
- package/src/detectors/cro.js +48 -0
- package/src/detectors/framework.js +31 -1
- 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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/commands/audit.js
CHANGED
|
@@ -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
|
+
}
|