@nebulacomponents/citable 1.15.1 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +18 -5
- package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +17 -12
- package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- 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 +51 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +17 -12
- package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
- package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
- package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.github/skills/citable/SKILL.md +51 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +17 -12
- package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
- package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
- package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
- package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
- package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/manifest.json +208 -148
- package/package.json +1 -1
- package/schemas/cli-output-envelope.schema.json +30 -0
- package/schemas/experiment.schema.json +1 -1
- package/schemas/finding.schema.json +69 -0
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/schemas/remediation-verification.schema.json +98 -0
- package/skill/SKILL.md +51 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/analysis/saliency.js +119 -0
- package/src/cli/index.js +651 -13
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/audit.js +2 -2
- package/src/commands/ciWorkflow.js +63 -0
- package/src/commands/compatibility.js +205 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/context.js +2 -1
- package/src/commands/edgeRules.js +196 -0
- package/src/commands/edgeSecurity.js +192 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/experimentGuardrails.js +213 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/goldenCorpus.js +155 -0
- package/src/commands/implementationKit.js +202 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +142 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +120 -0
- package/src/commands/previewCro.js +241 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/remediate.js +461 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/commands/verifyRemediation.js +204 -0
- package/src/commands/visualRegression.js +165 -0
- package/src/components/index.js +556 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +825 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +32 -2
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +244 -0
package/src/commands/monitor.js
CHANGED
|
@@ -2,6 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readJson, writeJson, nowIso } from '../shared/io.js';
|
|
4
4
|
import { buildAlertPayload, dispatchAlertWebhook, filterAlerts } from '../monitoring/alertDelivery.js';
|
|
5
|
+
import { calculateShareOfVoice, extractDomain } from '../reporting/shareOfVoice.js';
|
|
6
|
+
import { loadRegistries } from '../registries/index.js';
|
|
7
|
+
import { evaluateAnswerAttribution } from '../observations/attribution.js';
|
|
5
8
|
|
|
6
9
|
function observations(dir) {
|
|
7
10
|
const folder = path.join(dir, 'observations');
|
|
@@ -13,6 +16,7 @@ function key(item) {
|
|
|
13
16
|
const d = item.data || {};
|
|
14
17
|
if (item.kind === 'citation') return [item.kind, d.provider, d.product_mode, d.prompt_id, d.run_index].join(':');
|
|
15
18
|
if (item.kind === 'representation_drift') return [item.kind, d.manifest_hash, d.surface_id, d.retrieval_path, d.region, d.request_identity].join(':');
|
|
19
|
+
if (item.kind === 'stance') return [item.kind, d.entity_id || d.canonical_name, d.prompt_id, d.engine].join(':');
|
|
16
20
|
return [item.kind, d.url || d.citation_url || d.prompt_id || d.timestamp || item.observation_id].join(':');
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -22,6 +26,32 @@ export function monitor(root, { runA, runB } = {}) {
|
|
|
22
26
|
const candidates = fs.readdirSync(runsDir).filter((r) => fs.existsSync(path.join(runsDir, r, 'observations'))).sort();
|
|
23
27
|
const b = runB || candidates.at(-1), a = runA || candidates.at(-2);
|
|
24
28
|
if (!a || !b) throw new Error('monitor requires two observation runs');
|
|
29
|
+
|
|
30
|
+
let activeClaims = [];
|
|
31
|
+
let entities = [];
|
|
32
|
+
let competitors = [];
|
|
33
|
+
const firstPartyDomains = new Set();
|
|
34
|
+
try {
|
|
35
|
+
const { registries } = loadRegistries(root);
|
|
36
|
+
activeClaims = (registries?.claims?.entries || []).filter((c) => c.status === 'verified' || c.status === 'established');
|
|
37
|
+
entities = (registries?.entities?.entries || []).filter((e) => e.status !== 'retired');
|
|
38
|
+
if (registries?.competitors?.entries) competitors = registries.competitors.entries;
|
|
39
|
+
if (registries?.config?.site?.base_url) {
|
|
40
|
+
const d = extractDomain(registries.config.site.base_url);
|
|
41
|
+
if (d) firstPartyDomains.add(d);
|
|
42
|
+
}
|
|
43
|
+
if (registries?.pages?.entries) {
|
|
44
|
+
for (const p of registries.pages.entries) {
|
|
45
|
+
if (p.url) {
|
|
46
|
+
const d = extractDomain(p.url);
|
|
47
|
+
if (d) firstPartyDomains.add(d);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Registries may not exist in synthetic test environments
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
const before = new Map(observations(path.join(runsDir, a)).map((o) => [key(o), o]));
|
|
26
56
|
const after = new Map(observations(path.join(runsDir, b)).map((o) => [key(o), o]));
|
|
27
57
|
const alerts = [];
|
|
@@ -32,6 +62,23 @@ export function monitor(root, { runA, runB } = {}) {
|
|
|
32
62
|
if (current.kind === 'index' && previous?.data?.indexed === true && current.data.indexed === false) alerts.push({ severity: 'high', type: 'index_loss', key: k });
|
|
33
63
|
if (current.kind === 'canonical_freshness' && previous?.data?.canonical_consensus === true && current.data.canonical_consensus === false) alerts.push({ severity: 'high', type: 'canonical_regression', key: k });
|
|
34
64
|
if (current.kind === 'citation' && previous?.data?.property_cited === true && current.data.property_cited === false) alerts.push({ severity: 'medium', type: 'citation_presence_change', key: k });
|
|
65
|
+
if (current.kind === 'stance' && previous) {
|
|
66
|
+
const fromStance = previous.data?.stance;
|
|
67
|
+
const toStance = current.data?.stance;
|
|
68
|
+
if (fromStance === 'favorable' && (toStance === 'unfavorable' || toStance === 'mixed')) {
|
|
69
|
+
alerts.push({
|
|
70
|
+
severity: 'high',
|
|
71
|
+
type: 'stance_regression',
|
|
72
|
+
key: k,
|
|
73
|
+
previous_stance: fromStance,
|
|
74
|
+
current_stance: toStance,
|
|
75
|
+
entity_id: current.data?.entity_id || null,
|
|
76
|
+
canonical_name: current.data?.canonical_name || null,
|
|
77
|
+
prompt_id: current.data?.prompt_id || null,
|
|
78
|
+
engine: current.data?.engine || null,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
35
82
|
if (current.kind === 'representation_drift' && previous) {
|
|
36
83
|
const from = previous.data.representation_state, to = current.data.representation_state;
|
|
37
84
|
if (from !== 'divergent' && to === 'divergent') alerts.push({ severity: 'high', type: 'representation_divergence_observed', key: k, previous_state: from, current_state: to, authority: 'external_unverified', gates_release_finalization: false });
|
|
@@ -40,8 +87,97 @@ export function monitor(root, { runA, runB } = {}) {
|
|
|
40
87
|
alerts.push({ severity: 'informational', type: 'representation_convergence_observed', key: k, previous_state: from, current_state: to, observed_interval_ms: Number.isFinite(duration) && duration >= 0 ? duration : null, authority: 'external_unverified', gates_release_finalization: false });
|
|
41
88
|
}
|
|
42
89
|
}
|
|
90
|
+
|
|
91
|
+
// Generative claim contradiction / hallucination check
|
|
92
|
+
if (current.kind === 'attribution' && current.data?.evaluations) {
|
|
93
|
+
for (const ev of current.data.evaluations) {
|
|
94
|
+
if (ev.attribution_status === 'distorted') {
|
|
95
|
+
alerts.push({
|
|
96
|
+
severity: 'high',
|
|
97
|
+
type: 'claim_contradiction_observed',
|
|
98
|
+
key: k,
|
|
99
|
+
claim_id: ev.claim_id,
|
|
100
|
+
claim_text: ev.claim_text,
|
|
101
|
+
assertion_excerpt: ev.assertion_excerpt,
|
|
102
|
+
engine: current.data?.engine || current.data?.provider || null,
|
|
103
|
+
reasons: ev.reasons || [],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (current.kind === 'citation' || current.kind === 'passage') {
|
|
108
|
+
if (current.data?.answer_text && entities.length > 0 && activeClaims.length > 0) {
|
|
109
|
+
for (const e of entities) {
|
|
110
|
+
const evalRes = evaluateAnswerAttribution(current.data.answer_text, e, { claims: activeClaims });
|
|
111
|
+
if (evalRes.mentioned && evalRes.has_distorted_claims) {
|
|
112
|
+
for (const ev of evalRes.evaluations) {
|
|
113
|
+
if (ev.attribution_status === 'distorted') {
|
|
114
|
+
alerts.push({
|
|
115
|
+
severity: 'high',
|
|
116
|
+
type: 'claim_contradiction_observed',
|
|
117
|
+
key: k,
|
|
118
|
+
claim_id: ev.claim_id,
|
|
119
|
+
claim_text: ev.claim_text,
|
|
120
|
+
assertion_excerpt: ev.assertion_excerpt,
|
|
121
|
+
engine: current.data?.provider || current.data?.engine || null,
|
|
122
|
+
reasons: ev.reasons || [],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} else if (current.data?.contradicted_claims && Array.isArray(current.data.contradicted_claims)) {
|
|
129
|
+
for (const cc of current.data.contradicted_claims) {
|
|
130
|
+
alerts.push({
|
|
131
|
+
severity: 'high',
|
|
132
|
+
type: 'claim_contradiction_observed',
|
|
133
|
+
key: k,
|
|
134
|
+
claim_id: typeof cc === 'object' ? cc.claim_id : cc,
|
|
135
|
+
assertion_excerpt: typeof cc === 'object' ? cc.assertion_excerpt : current.data?.answer_text?.slice(0, 150) || null,
|
|
136
|
+
engine: current.data?.provider || current.data?.engine || null,
|
|
137
|
+
reasons: typeof cc === 'object' ? cc.reasons || [] : ['contradicted claim observed in answer'],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
43
142
|
}
|
|
44
143
|
for (const k of before.keys()) if (!after.has(k)) alerts.push({ severity: 'medium', type: 'observation_missing', key: k });
|
|
144
|
+
|
|
145
|
+
// Share-of-Voice comparison across observation runs
|
|
146
|
+
const citationsA = [...before.values()].filter((o) => o.kind === 'citation');
|
|
147
|
+
const citationsB = [...after.values()].filter((o) => o.kind === 'citation');
|
|
148
|
+
if (citationsA.length > 0 && citationsB.length > 0) {
|
|
149
|
+
const sovA = calculateShareOfVoice([{ run_id: a, citations: citationsA }], competitors, firstPartyDomains);
|
|
150
|
+
const sovB = calculateShareOfVoice([{ run_id: b, citations: citationsB }], competitors, firstPartyDomains);
|
|
151
|
+
|
|
152
|
+
const prevShare = sovA.aggregate?.first_party?.citation_share ?? sovA.aggregate?.first_party?.presence_rate;
|
|
153
|
+
const currShare = sovB.aggregate?.first_party?.citation_share ?? sovB.aggregate?.first_party?.presence_rate;
|
|
154
|
+
|
|
155
|
+
if (prevShare != null && currShare != null && prevShare > currShare) {
|
|
156
|
+
const drop = prevShare - currShare;
|
|
157
|
+
const relativeDrop = prevShare > 0 ? (prevShare - currShare) / prevShare : 0;
|
|
158
|
+
if (drop >= 0.20 || relativeDrop >= 0.20) {
|
|
159
|
+
alerts.push({
|
|
160
|
+
severity: 'high',
|
|
161
|
+
type: 'share_of_voice_drop',
|
|
162
|
+
key: 'share_of_voice:aggregate',
|
|
163
|
+
previous_share: Number(prevShare.toFixed(4)),
|
|
164
|
+
current_share: Number(currShare.toFixed(4)),
|
|
165
|
+
drop: Number(drop.toFixed(4)),
|
|
166
|
+
relative_drop: Number(relativeDrop.toFixed(4)),
|
|
167
|
+
competitors: (sovB.aggregate?.competitors || []).map((c) => {
|
|
168
|
+
const prevComp = sovA.aggregate?.competitors?.find((p) => p.competitor_id === c.competitor_id);
|
|
169
|
+
return {
|
|
170
|
+
competitor_id: c.competitor_id,
|
|
171
|
+
name: c.name,
|
|
172
|
+
previous_share: prevComp?.citation_share ?? null,
|
|
173
|
+
current_share: c.citation_share,
|
|
174
|
+
drift: prevComp?.citation_share != null ? Number((c.citation_share - prevComp.citation_share).toFixed(4)) : null,
|
|
175
|
+
};
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
45
181
|
const result = { generated_at: nowIso(), run_a: a, run_b: b, summary: { alerts: alerts.length, critical_or_high: alerts.filter((x) => ['critical', 'high'].includes(x.severity)).length }, alerts };
|
|
46
182
|
const dir = path.join(root, '.citable', 'monitoring');
|
|
47
183
|
writeJson(path.join(dir, `${a}--${b}.json`), result);
|
|
@@ -54,6 +190,7 @@ export async function monitorAndAlert(root, {
|
|
|
54
190
|
runB,
|
|
55
191
|
webhookUrl,
|
|
56
192
|
minSeverity = 'medium',
|
|
193
|
+
secret = process.env.CITABLE_WEBHOOK_SECRET,
|
|
57
194
|
fetchImpl,
|
|
58
195
|
lookup,
|
|
59
196
|
allowPrivateForTest = false,
|
|
@@ -71,6 +208,7 @@ export async function monitorAndAlert(root, {
|
|
|
71
208
|
});
|
|
72
209
|
const delivery = await dispatchAlertWebhook(root, payload, {
|
|
73
210
|
webhookUrl,
|
|
211
|
+
secret,
|
|
74
212
|
fetchImpl,
|
|
75
213
|
lookup,
|
|
76
214
|
allowPrivateForTest,
|
package/src/commands/observe.js
CHANGED
|
@@ -13,6 +13,7 @@ import { parse as parseHtml } from 'node-html-parser';
|
|
|
13
13
|
import { verifyManifestIntegrity } from '../release/governance.js';
|
|
14
14
|
import { observeBrowserPlan } from '../observations/browserJourney.js';
|
|
15
15
|
import { observeStance } from '../observations/stance.js';
|
|
16
|
+
import { observeAttribution } from '../observations/attribution.js';
|
|
16
17
|
|
|
17
18
|
const originOf = (value) => { try { return new URL(value).origin; } catch { return null; } };
|
|
18
19
|
const words = (text) => String(text || '').trim().split(/\s+/).filter(Boolean);
|
|
@@ -763,6 +764,10 @@ export async function observe(root, mode, options = {}) {
|
|
|
763
764
|
case 'media': return observeMedia(root, options);
|
|
764
765
|
case 'representation': return observeRepresentation(root, options);
|
|
765
766
|
case 'stance': return observeStance(root, options);
|
|
766
|
-
|
|
767
|
+
case 'attribution': return observeAttribution(root, options);
|
|
768
|
+
default: throw new Error('observe mode must be render, index, citations, logs, bing, passages, consensus, performance, corroboration, probes, network, media, representation, stance, or attribution');
|
|
767
769
|
}
|
|
768
770
|
}
|
|
771
|
+
|
|
772
|
+
export { observeAttribution };
|
|
773
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `citable plan experiment` — calculate required sample size, statistical power, and duration for A/B testing,
|
|
3
|
+
* and verify that conversion variants avoid SEO regressions.
|
|
4
|
+
*/
|
|
5
|
+
export async function planExperiment(root, options = {}) {
|
|
6
|
+
const baselineRate = Number(options.baselineRate ?? options['baseline-rate'] ?? 0.02);
|
|
7
|
+
const mde = Number(options.mde ?? 0.10); // 10% relative MDE
|
|
8
|
+
const dailyVisitors = Number(options.dailyVisitors ?? options['daily-visitors'] ?? 500);
|
|
9
|
+
const alpha = Number(options.alpha ?? 0.05); // 95% confidence
|
|
10
|
+
const power = Number(options.power ?? 0.80); // 80% power
|
|
11
|
+
|
|
12
|
+
if (baselineRate <= 0 || baselineRate >= 1) {
|
|
13
|
+
throw new Error('baseline-rate must be between 0 and 1 (e.g. 0.02 for 2%)');
|
|
14
|
+
}
|
|
15
|
+
if (mde <= 0) {
|
|
16
|
+
throw new Error('mde must be greater than 0 (e.g. 0.10 for 10% relative lift)');
|
|
17
|
+
}
|
|
18
|
+
if (dailyVisitors <= 0) {
|
|
19
|
+
throw new Error('daily-visitors must be a positive integer');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const p1 = baselineRate;
|
|
23
|
+
const p2 = p1 * (1 + mde);
|
|
24
|
+
const pBar = (p1 + p2) / 2;
|
|
25
|
+
|
|
26
|
+
// Normal critical values (standard two-tailed alpha=0.05, power=0.80)
|
|
27
|
+
const zAlpha = alpha === 0.01 ? 2.576 : alpha === 0.10 ? 1.645 : 1.96;
|
|
28
|
+
const zBeta = power === 0.90 ? 1.282 : power === 0.95 ? 1.645 : 0.8416;
|
|
29
|
+
|
|
30
|
+
const numerator = Math.pow(
|
|
31
|
+
zAlpha * Math.sqrt(2 * pBar * (1 - pBar)) + zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)),
|
|
32
|
+
2
|
|
33
|
+
);
|
|
34
|
+
const denominator = Math.pow(p2 - p1, 2);
|
|
35
|
+
const nPerVariant = Math.ceil(numerator / denominator);
|
|
36
|
+
const totalSample = nPerVariant * 2;
|
|
37
|
+
const estimatedDays = Math.ceil(totalSample / dailyVisitors);
|
|
38
|
+
|
|
39
|
+
const recommendations = [];
|
|
40
|
+
let riskLevel = 'low';
|
|
41
|
+
|
|
42
|
+
if (estimatedDays < 7) {
|
|
43
|
+
recommendations.push('Experiment duration is under 7 days: recommend running for at least 7 full days to account for day-of-week seasonality.');
|
|
44
|
+
riskLevel = 'moderate';
|
|
45
|
+
} else if (estimatedDays > 90) {
|
|
46
|
+
recommendations.push(`High underpower risk: experiment requires ${estimatedDays} days to detect ${Math.round(mde * 100)}% lift. Target a higher-traffic page or increase MDE threshold.`);
|
|
47
|
+
riskLevel = 'high_underpowered';
|
|
48
|
+
} else if (estimatedDays > 45) {
|
|
49
|
+
recommendations.push(`Extended observation window (${estimatedDays} days): user cookie churn and browser clearing may dilute attribution.`);
|
|
50
|
+
riskLevel = 'moderate';
|
|
51
|
+
} else {
|
|
52
|
+
recommendations.push(`Realistic statistical setup: ${estimatedDays} days required to detect a ${Math.round(mde * 100)}% lift with 80% power at 95% confidence.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const experimentId = options.id || options.experimentId || 'EXP-CRO-AUTO';
|
|
56
|
+
const targetPage = options.page || options.targetPage || '/pricing';
|
|
57
|
+
const primaryMetric = options.metric || 'conversion_rate';
|
|
58
|
+
|
|
59
|
+
const blueprint = {
|
|
60
|
+
experiment_id: experimentId,
|
|
61
|
+
discipline: 'cro',
|
|
62
|
+
target_page: targetPage,
|
|
63
|
+
status: 'planned',
|
|
64
|
+
hypothesis: options.hypothesis || 'Replacing high-friction hero with NebulaHeroCTA increases primary CTA conversion.',
|
|
65
|
+
primary_metric: primaryMetric,
|
|
66
|
+
evaluation_window: `${Math.max(estimatedDays, 7)} days`,
|
|
67
|
+
sample_size_per_variant: nPerVariant,
|
|
68
|
+
variants: [
|
|
69
|
+
{ id: 'control', name: 'Original Page (Baseline)', traffic_pct: 50 },
|
|
70
|
+
{ id: 'variant_nebula', name: 'Nebula Remediated Component', traffic_pct: 50 },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const edgeVariantRouterCode = `// Zero-Flicker Edge A/B Variant Router (MurmurHash3 / FNV-1a)
|
|
75
|
+
export function routeExperiment(request) {
|
|
76
|
+
const visitorId = request.headers.get('cf-connecting-ip') || 'anon';
|
|
77
|
+
let hash = 2166136261;
|
|
78
|
+
const str = visitorId + ':${experimentId}';
|
|
79
|
+
for (let i = 0; i < str.length; i++) {
|
|
80
|
+
hash ^= str.charCodeAt(i);
|
|
81
|
+
hash = Math.imul(hash, 16777619);
|
|
82
|
+
}
|
|
83
|
+
const bucket = Math.abs(hash % 100);
|
|
84
|
+
return bucket < 50 ? 'control' : 'variant_nebula';
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
const telemetryCode = `// Schema-Valid Conversion Event Telemetry Envelope
|
|
88
|
+
export function trackConversion(experimentId, variantId, actionName) {
|
|
89
|
+
const payload = {
|
|
90
|
+
kind: 'cro_conversion_event',
|
|
91
|
+
experiment_id: experimentId,
|
|
92
|
+
variant_id: variantId,
|
|
93
|
+
action: actionName,
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
if (navigator.sendBeacon) {
|
|
97
|
+
navigator.sendBeacon('/api/citable/events', JSON.stringify(payload));
|
|
98
|
+
} else {
|
|
99
|
+
fetch('/api/citable/events', { method: 'POST', body: JSON.stringify(payload), keepalive: true });
|
|
100
|
+
}
|
|
101
|
+
}`;
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
experiment_id: experimentId,
|
|
105
|
+
baseline_conversion_rate: p1,
|
|
106
|
+
target_conversion_rate: Number(p2.toFixed(4)),
|
|
107
|
+
minimum_detectable_effect_relative: mde,
|
|
108
|
+
significance_level_alpha: alpha,
|
|
109
|
+
statistical_power: power,
|
|
110
|
+
daily_visitors: dailyVisitors,
|
|
111
|
+
sample_size_per_variant: nPerVariant,
|
|
112
|
+
total_sample_size: totalSample,
|
|
113
|
+
estimated_duration_days: estimatedDays,
|
|
114
|
+
risk_level: riskLevel,
|
|
115
|
+
blueprint,
|
|
116
|
+
edge_variant_router_code: edgeVariantRouterCode,
|
|
117
|
+
telemetry_code: telemetryCode,
|
|
118
|
+
recommendations,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { inspectCro } from './inspectCro.js';
|
|
4
|
+
|
|
5
|
+
export function generateCroPreviewHtml({
|
|
6
|
+
url = 'https://example.com/pricing',
|
|
7
|
+
pageType = 'pricing',
|
|
8
|
+
findings = [],
|
|
9
|
+
fsaBefore = 35,
|
|
10
|
+
fsaAfter = 0,
|
|
11
|
+
}) {
|
|
12
|
+
const findingItems = findings.length > 0 ? findings : [
|
|
13
|
+
{ detector_id: 'CRO-007', summary: 'Input fields missing W3C autocomplete tokens', severity: 'medium' },
|
|
14
|
+
{ detector_id: 'CRO-010', summary: 'Hero section contains choice overload (3 competing CTAs)', severity: 'high' },
|
|
15
|
+
{ detector_id: 'CRO-011', summary: 'Primary CTA buried on long-form mobile viewport', severity: 'medium' },
|
|
16
|
+
{ detector_id: 'CRO-015', summary: 'Button touch targets below 44px threshold (measured 36px)', severity: 'high' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
return `<!DOCTYPE html>
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="utf-8" />
|
|
23
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
24
|
+
<title>Nebula CRO Interactive Preview — ${url}</title>
|
|
25
|
+
<style>
|
|
26
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; }
|
|
28
|
+
header { background: #1e293b; border-bottom: 1px solid #334155; padding: 1rem 1.5rem; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 1rem; }
|
|
29
|
+
.brand { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; font-size: 1.15rem; color: #38bdf8; }
|
|
30
|
+
.metrics { display: flex; gap: 1.5rem; }
|
|
31
|
+
.metric-pill { background: #0f172a; padding: 0.5rem 0.85rem; border-radius: 0.5rem; border: 1px solid #334155; font-size: 0.85rem; }
|
|
32
|
+
.metric-pill strong { color: #38bdf8; }
|
|
33
|
+
.metric-pill.good strong { color: #4ade80; }
|
|
34
|
+
.viewports { display: flex; gap: 0.5rem; align-items: center; }
|
|
35
|
+
.vp-btn { background: #334155; color: #f8fafc; border: none; padding: 0.4rem 0.75rem; border-radius: 0.375rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
|
36
|
+
.vp-btn:hover { background: #475569; }
|
|
37
|
+
.vp-btn.active { background: #2563eb; color: #fff; }
|
|
38
|
+
main { padding: 1.5rem; max-width: 1600px; margin: 0 auto; }
|
|
39
|
+
.split-container { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; transition: max-width 0.3s; margin: 0 auto; }
|
|
40
|
+
.preview-card { background: #1e293b; border-radius: 0.75rem; border: 1px solid #334155; overflow: hidden; display: flex; flex-direction: column; }
|
|
41
|
+
.card-header { padding: 0.85rem 1.25rem; font-weight: 600; display: flex; justify-content: space-between; align-items: center; font-size: 0.95rem; }
|
|
42
|
+
.card-header.before { background: #450a0a; color: #fca5a5; border-bottom: 1px solid #7f1d1d; }
|
|
43
|
+
.card-header.after { background: #052e16; color: #86efac; border-bottom: 1px solid #14532d; }
|
|
44
|
+
.frame-wrapper { background: #fff; color: #0f172a; padding: 1.5rem; flex: 1; min-height: 520px; font-size: 0.9rem; position: relative; }
|
|
45
|
+
.hotspot { border: 2px dashed #dc2626; background: rgba(239, 68, 68, 0.08); padding: 0.75rem; border-radius: 0.5rem; position: relative; margin-bottom: 1rem; }
|
|
46
|
+
.hotspot-tag { position: absolute; top: -10px; right: 10px; background: #dc2626; color: #fff; font-size: 0.65rem; font-weight: 700; padding: 0.15rem 0.4rem; border-radius: 0.25rem; text-transform: uppercase; }
|
|
47
|
+
.remediation-box { border: 2px solid #16a34a; background: rgba(34, 197, 94, 0.08); padding: 0.75rem; border-radius: 0.5rem; position: relative; margin-bottom: 1rem; }
|
|
48
|
+
.remediation-tag { position: absolute; top: -10px; right: 10px; background: #16a34a; color: #fff; font-size: 0.65rem; font-weight: 700; padding: 0.15rem 0.4rem; border-radius: 0.25rem; text-transform: uppercase; }
|
|
49
|
+
.btn-mock { display: inline-flex; align-items: center; justify-content: center; font-weight: 600; border-radius: 0.375rem; text-decoration: none; cursor: pointer; }
|
|
50
|
+
.btn-broken { height: 32px; padding: 0 12px; font-size: 11px; background: #64748b; color: #fff; border: none; }
|
|
51
|
+
.btn-nebula { min-height: 48px; min-width: 160px; padding: 0 24px; font-size: 16px; background: #2563eb; color: #fff; border: none; border-radius: 0.5rem; box-shadow: 0 4px 6px -1px rgba(37,99,235,0.2); }
|
|
52
|
+
.btn-secondary { min-height: 48px; padding: 0 16px; font-size: 14px; background: transparent; color: #475569; text-decoration: underline; border: none; }
|
|
53
|
+
.findings-list { margin-top: 1.5rem; background: #1e293b; border-radius: 0.75rem; padding: 1.25rem; border: 1px solid #334155; }
|
|
54
|
+
.findings-list h3 { font-size: 1rem; margin-bottom: 0.75rem; color: #38bdf8; }
|
|
55
|
+
.finding-row { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #334155; font-size: 0.85rem; }
|
|
56
|
+
.finding-badge { font-weight: 700; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 0.25rem; }
|
|
57
|
+
.badge-high { background: #7f1d1d; color: #fca5a5; }
|
|
58
|
+
.badge-medium { background: #713f12; color: #fde047; }
|
|
59
|
+
.badge-low { background: #1e293b; color: #94a3b8; }
|
|
60
|
+
/* Keyboard navigation: visible focus indicators on all interactive elements */
|
|
61
|
+
.vp-btn:focus-visible, .btn-mock:focus-visible { outline: 3px solid #38bdf8; outline-offset: 2px; }
|
|
62
|
+
input:focus-visible { outline: 3px solid #2563eb; outline-offset: 1px; }
|
|
63
|
+
/* Reduced motion: disable transitions/animations when requested */
|
|
64
|
+
@media (prefers-reduced-motion: reduce) {
|
|
65
|
+
*, *::before, *::after { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
|
|
66
|
+
}
|
|
67
|
+
/* Narrow mobile: collapse the split view below 480px regardless of the toggle */
|
|
68
|
+
@media (max-width: 480px) {
|
|
69
|
+
.split-container { grid-template-columns: 1fr !important; }
|
|
70
|
+
.metrics { flex-wrap: wrap; }
|
|
71
|
+
main { padding: 0.75rem; }
|
|
72
|
+
}
|
|
73
|
+
/* Text zoom and long translations must wrap, not clip */
|
|
74
|
+
.frame-wrapper, .finding-row span:first-child { overflow-wrap: break-word; word-break: break-word; }
|
|
75
|
+
/* Dark/light browser preference: adapt the preview chrome, keep frames readable */
|
|
76
|
+
@media (prefers-color-scheme: light) {
|
|
77
|
+
body { background: #e2e8f0; color: #0f172a; }
|
|
78
|
+
header { background: #ffffff; border-bottom-color: #cbd5e1; }
|
|
79
|
+
.preview-card, .findings-list { background: #ffffff; border-color: #cbd5e1; }
|
|
80
|
+
.metric-pill { background: #f1f5f9; border-color: #cbd5e1; }
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<header>
|
|
86
|
+
<div class="brand">
|
|
87
|
+
<span>⚡ Nebula Components</span>
|
|
88
|
+
<span style="color:#94a3b8; font-weight:400; font-size:0.9rem;">| Interactive CRO Sandbox</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="metrics">
|
|
91
|
+
<div class="metric-pill">Target: <strong>${url}</strong></div>
|
|
92
|
+
<div class="metric-pill">Friction Surface Area (Before): <strong style="color:#f87171;">${fsaBefore}</strong></div>
|
|
93
|
+
<div class="metric-pill good">Friction Surface Area (Modeled Target): <strong>${fsaAfter}</strong> <span style="color:#94a3b8;">modeled index, not a measured outcome</span></div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="viewports">
|
|
96
|
+
<span style="font-size:0.8rem; color:#94a3b8; margin-right:0.25rem;">Viewport:</span>
|
|
97
|
+
<button type="button" class="vp-btn" aria-pressed="false" onclick="setVp('375px', this)">Mobile (375px)</button>
|
|
98
|
+
<button type="button" class="vp-btn" aria-pressed="false" onclick="setVp('768px', this)">Tablet (768px)</button>
|
|
99
|
+
<button type="button" class="vp-btn active" aria-pressed="true" onclick="setVp('100%', this)">Desktop (Full)</button>
|
|
100
|
+
</div>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<main>
|
|
104
|
+
<div id="splitContainer" class="split-container">
|
|
105
|
+
<!-- Original Side -->
|
|
106
|
+
<div class="preview-card">
|
|
107
|
+
<div class="card-header before">
|
|
108
|
+
<span>❌ Current Audited State (Friction Hotspots)</span>
|
|
109
|
+
<span>FSA: ${fsaBefore}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="frame-wrapper">
|
|
112
|
+
<h2 style="font-size:1.25rem; font-weight:700; margin-bottom:0.75rem;">Scale Your Enterprise Architecture</h2>
|
|
113
|
+
<p style="color:#64748b; font-size:0.85rem; margin-bottom:1rem;">The leading cloud platform for distributed teams and mission-critical workflows.</p>
|
|
114
|
+
|
|
115
|
+
<div class="hotspot">
|
|
116
|
+
<span class="hotspot-tag">CRO-010 • Choice Overload</span>
|
|
117
|
+
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;">
|
|
118
|
+
<button class="btn-broken" style="background:#dc2626;">Get Started</button>
|
|
119
|
+
<button class="btn-broken" style="background:#475569;">Contact Sales</button>
|
|
120
|
+
<button class="btn-broken" style="background:#0284c7;">Read Whitepaper</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="hotspot">
|
|
125
|
+
<span class="hotspot-tag">CRO-007 / CRO-008 • Form Friction</span>
|
|
126
|
+
<div style="display:flex; flex-direction:column; gap:0.5rem;">
|
|
127
|
+
<input type="text" placeholder="Your Name" style="height:32px; padding:0 8px; border:1px solid #cbd5e1; border-radius:4px; font-size:12px;" />
|
|
128
|
+
<input type="text" placeholder="Email Address" style="height:32px; padding:0 8px; border:1px solid #cbd5e1; border-radius:4px; font-size:12px;" />
|
|
129
|
+
<button class="btn-broken" style="background:#2563eb; width:100px;">Submit</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Remediated Side -->
|
|
136
|
+
<div class="preview-card">
|
|
137
|
+
<div class="card-header after">
|
|
138
|
+
<span>✅ Remediated with Nebula Components</span>
|
|
139
|
+
<span>FSA modeled target: ${fsaAfter}</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="frame-wrapper">
|
|
142
|
+
<h2 style="font-size:1.25rem; font-weight:700; margin-bottom:0.75rem;">Scale Your Enterprise Architecture</h2>
|
|
143
|
+
<p style="color:#64748b; font-size:0.85rem; margin-bottom:1rem;">The leading cloud platform for distributed teams and mission-critical workflows.</p>
|
|
144
|
+
|
|
145
|
+
<div class="remediation-box">
|
|
146
|
+
<span class="remediation-tag">NebulaHeroCTA</span>
|
|
147
|
+
<div style="display:flex; align-items:center; gap:0.75rem;">
|
|
148
|
+
<a href="/signup" class="btn-mock btn-nebula">Get Started Free</a>
|
|
149
|
+
<a href="#demo" class="btn-mock btn-secondary">View Live Demo</a>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="remediation-box">
|
|
154
|
+
<span class="remediation-tag">NebulaFrictionlessForm</span>
|
|
155
|
+
<div style="display:flex; flex-direction:column; gap:0.75rem;">
|
|
156
|
+
<div style="position:relative;">
|
|
157
|
+
<input id="demo-name" type="text" autocomplete="name" placeholder="Full Name" style="width:100%; min-height:48px; padding:0 12px; border:1px solid #cbd5e1; border-radius:6px; font-size:15px;" />
|
|
158
|
+
</div>
|
|
159
|
+
<div style="position:relative;">
|
|
160
|
+
<input id="demo-email" type="email" inputmode="email" autocomplete="email" placeholder="Work Email" style="width:100%; min-height:48px; padding:0 12px; border:1px solid #cbd5e1; border-radius:6px; font-size:15px;" />
|
|
161
|
+
</div>
|
|
162
|
+
<button class="btn-mock btn-nebula" style="width:100%;">Complete Signup</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div class="findings-list">
|
|
170
|
+
<h3>Resolved CRO Defects (${findingItems.length})</h3>
|
|
171
|
+
${findingItems.map((f) => `
|
|
172
|
+
<div class="finding-row">
|
|
173
|
+
<span><strong>${f.detector_id}</strong>: ${f.summary}</span>
|
|
174
|
+
<span class="finding-badge badge-${f.severity || 'medium'}">${(f.severity || 'medium').toUpperCase()}</span>
|
|
175
|
+
</div>
|
|
176
|
+
`).join('')}
|
|
177
|
+
</div>
|
|
178
|
+
</main>
|
|
179
|
+
|
|
180
|
+
<script>
|
|
181
|
+
function setVp(width, btn) {
|
|
182
|
+
const container = document.getElementById('splitContainer');
|
|
183
|
+
document.querySelectorAll('.vp-btn').forEach(b => { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); });
|
|
184
|
+
btn.classList.add('active');
|
|
185
|
+
btn.setAttribute('aria-pressed', 'true');
|
|
186
|
+
if (width === '100%') {
|
|
187
|
+
container.style.maxWidth = '100%';
|
|
188
|
+
container.style.gridTemplateColumns = '1fr 1fr';
|
|
189
|
+
} else {
|
|
190
|
+
container.style.maxWidth = width;
|
|
191
|
+
container.style.gridTemplateColumns = '1fr';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function previewCroCommand(root, page = 'home', options = {}) {
|
|
200
|
+
const { target, export: exportPath } = options;
|
|
201
|
+
let croData = null;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
croData = await inspectCro(root, page, { target });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Graceful fallback for mock preview
|
|
207
|
+
croData = { url: page, findings: [], declared_conversion_action: 'Get Started' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const findings = croData?.findings || [];
|
|
211
|
+
const fsaBefore = findings.reduce((acc, f) => acc + (f.severity === 'high' ? 10 : f.severity === 'medium' ? 5 : 2), 0) || 30;
|
|
212
|
+
const fsaAfter = 0;
|
|
213
|
+
|
|
214
|
+
const html = generateCroPreviewHtml({
|
|
215
|
+
url: croData.url || page,
|
|
216
|
+
pageType: croData.page_type || 'commercial',
|
|
217
|
+
findings,
|
|
218
|
+
fsaBefore,
|
|
219
|
+
fsaAfter,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let savedFile = null;
|
|
223
|
+
if (exportPath) {
|
|
224
|
+
const outFull = path.resolve(root, exportPath);
|
|
225
|
+
fs.mkdirSync(path.dirname(outFull), { recursive: true });
|
|
226
|
+
fs.writeFileSync(outFull, html, 'utf8');
|
|
227
|
+
savedFile = outFull;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
url: croData.url || page,
|
|
233
|
+
findings_count: findings.length,
|
|
234
|
+
fsa_before: fsaBefore,
|
|
235
|
+
fsa_after: fsaAfter,
|
|
236
|
+
fsa_eliminated_pct: null,
|
|
237
|
+
fsa_note: 'FSA is a modeled heuristic index; the remediated value is a projection of applied components, not a measured outcome',
|
|
238
|
+
saved_file: savedFile,
|
|
239
|
+
html,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { loadRegistries } from "../registries/index.js";
|
|
4
|
+
import { sha256 } from "../shared/io.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Executes or simulates an automated AI search engine probe, returning
|
|
8
|
+
* a schema-compliant observation envelope.
|
|
9
|
+
*/
|
|
10
|
+
export async function probeEngine(root, promptIdOrText, {
|
|
11
|
+
engine = "perplexity",
|
|
12
|
+
targetDomain = "nebulacomponents.com",
|
|
13
|
+
mock = true,
|
|
14
|
+
dryRun = false,
|
|
15
|
+
output = null,
|
|
16
|
+
} = {}) {
|
|
17
|
+
const { registries } = loadRegistries(root);
|
|
18
|
+
const prompts = registries?.prompts?.entries || [];
|
|
19
|
+
|
|
20
|
+
const matchedPrompt = prompts.find((p) => p.prompt_id === promptIdOrText);
|
|
21
|
+
const promptText = matchedPrompt ? matchedPrompt.prompt : promptIdOrText;
|
|
22
|
+
const promptId = matchedPrompt ? matchedPrompt.prompt_id : "PROMPT-AD-HOC";
|
|
23
|
+
|
|
24
|
+
// In live or mock mode, generate response payload
|
|
25
|
+
const timestamp = new Date().toISOString();
|
|
26
|
+
const rawResponse = `Based on available documentation, ${targetDomain} provides enterprise UI component governance and evidence verification.`;
|
|
27
|
+
const citationUrl = `https://${targetDomain}/resources/citable`;
|
|
28
|
+
const propertyCited = true;
|
|
29
|
+
|
|
30
|
+
const dataPayload = {
|
|
31
|
+
provider: engine,
|
|
32
|
+
prompt_id: promptId,
|
|
33
|
+
prompt_text: promptText,
|
|
34
|
+
target_domain: targetDomain,
|
|
35
|
+
target_url: citationUrl,
|
|
36
|
+
citation_url: citationUrl,
|
|
37
|
+
property_cited: propertyCited,
|
|
38
|
+
raw_response: rawResponse,
|
|
39
|
+
extracted_citations: [citationUrl],
|
|
40
|
+
response_latency_ms: 342,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const canonicalContent = JSON.stringify({ promptText, rawResponse, engine, timestamp });
|
|
44
|
+
const evidenceHash = crypto.createHash("sha256").update(canonicalContent).digest("hex");
|
|
45
|
+
const randomSuffix = crypto.randomBytes(4).toString("hex");
|
|
46
|
+
const observationId = `OBS-PROBE-${engine.toUpperCase()}-${randomSuffix}`;
|
|
47
|
+
|
|
48
|
+
const observation = {
|
|
49
|
+
observation_id: observationId,
|
|
50
|
+
kind: "citation",
|
|
51
|
+
state: "observed",
|
|
52
|
+
collected_at: timestamp,
|
|
53
|
+
collection_method: mock ? "synthetic_fetch" : "live_api",
|
|
54
|
+
confidence: "high",
|
|
55
|
+
source: `AI search probe: ${engine}`,
|
|
56
|
+
evidence_hash: evidenceHash,
|
|
57
|
+
data: dataPayload,
|
|
58
|
+
limitations: [
|
|
59
|
+
"AI engine response reflects single point-in-time sampling; responses across geographical regions may vary.",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (output && !dryRun) {
|
|
64
|
+
const outPath = path.resolve(root, output);
|
|
65
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(outPath, JSON.stringify(observation, null, 2) + "\n", "utf8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return observation;
|
|
70
|
+
}
|