@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
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { nowIso, readJson } from '../shared/io.js';
|
|
4
|
+
|
|
5
|
+
const NO_GUARANTEE = 'Consensus matrix evaluates agreement between declared publisher signals (HTML rel=canonical, Open Graph og:url, XML sitemaps, HTTP Last-Modified) and optional search engine observations. Signal consensus does not guarantee crawling, indexing, ranking, or citation outcomes in search or answer engines.';
|
|
6
|
+
|
|
7
|
+
export function loadConsensusRuns(root, { runId, since, last } = {}) {
|
|
8
|
+
const runsDir = path.join(root, '.citable', 'runs');
|
|
9
|
+
const included = [];
|
|
10
|
+
const skipped = [];
|
|
11
|
+
if (!fs.existsSync(runsDir)) return { included, skipped };
|
|
12
|
+
|
|
13
|
+
let runIds = [];
|
|
14
|
+
if (runId) {
|
|
15
|
+
if (fs.existsSync(path.join(runsDir, runId))) {
|
|
16
|
+
runIds = [runId];
|
|
17
|
+
} else {
|
|
18
|
+
throw new Error(`run not found: ${runId}`);
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
runIds = fs.readdirSync(runsDir).sort();
|
|
22
|
+
if (since) runIds = runIds.filter((id) => id >= since);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const id of runIds) {
|
|
26
|
+
const dir = path.join(runsDir, id);
|
|
27
|
+
const obsDir = path.join(dir, 'observations');
|
|
28
|
+
if (!fs.existsSync(obsDir)) continue;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const files = fs.readdirSync(obsDir).filter((f) => f.endsWith('.json')).sort();
|
|
32
|
+
const consensusObs = [];
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
const item = readJson(path.join(obsDir, f));
|
|
35
|
+
if (item.kind === 'canonical_freshness') {
|
|
36
|
+
consensusObs.push(item);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (consensusObs.length > 0) {
|
|
40
|
+
const manifest = fs.existsSync(path.join(dir, 'manifest.json'))
|
|
41
|
+
? readJson(path.join(dir, 'manifest.json'))
|
|
42
|
+
: null;
|
|
43
|
+
included.push({ run_id: id, manifest, observations: consensusObs });
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
skipped.push({ run_id: id, reason: err.message });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const windowed = Number.isInteger(last) && last > 0 ? included.slice(-last) : included;
|
|
51
|
+
return { included: windowed, skipped };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildConsensusMatrix(includedRuns = []) {
|
|
55
|
+
if (!includedRuns.length) {
|
|
56
|
+
return {
|
|
57
|
+
latest_run_id: null,
|
|
58
|
+
target: null,
|
|
59
|
+
total_urls: 0,
|
|
60
|
+
full_canonical_consensus: 0,
|
|
61
|
+
canonical_conflicts: 0,
|
|
62
|
+
engine_discrepancies: 0,
|
|
63
|
+
freshness_aligned: 0,
|
|
64
|
+
freshness_conflicts: 0,
|
|
65
|
+
freshness_insufficient: 0,
|
|
66
|
+
rows: [],
|
|
67
|
+
discrepancies: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const latestRun = includedRuns[includedRuns.length - 1];
|
|
72
|
+
const observations = latestRun.observations || [];
|
|
73
|
+
|
|
74
|
+
const rows = [];
|
|
75
|
+
const discrepancies = [];
|
|
76
|
+
|
|
77
|
+
let fullCanonicalConsensus = 0;
|
|
78
|
+
let canonicalConflicts = 0;
|
|
79
|
+
let engineDiscrepancies = 0;
|
|
80
|
+
let freshnessAligned = 0;
|
|
81
|
+
let freshnessConflicts = 0;
|
|
82
|
+
let freshnessInsufficient = 0;
|
|
83
|
+
|
|
84
|
+
for (const obs of observations) {
|
|
85
|
+
const data = obs.data || {};
|
|
86
|
+
const signals = data.signals || {};
|
|
87
|
+
const url = data.url || signals.final_url;
|
|
88
|
+
const htmlCanonical = signals.html_canonical || null;
|
|
89
|
+
const ogUrl = signals.open_graph_url || null;
|
|
90
|
+
const sitemapPresent = Boolean(signals.sitemap_present);
|
|
91
|
+
const canonicalConsensus = Boolean(data.canonical_consensus);
|
|
92
|
+
const dateConsensus = data.date_consensus;
|
|
93
|
+
const freshnessAssessment = data.freshness_assessment || 'insufficient_signals';
|
|
94
|
+
const contentSnapshot = data.content_snapshot || {};
|
|
95
|
+
const engineCanonicals = data.engine_selected_canonical || [];
|
|
96
|
+
const canonicalConsensusWithEngines = data.canonical_consensus_with_engines;
|
|
97
|
+
|
|
98
|
+
const itemDiscrepancies = [];
|
|
99
|
+
if (htmlCanonical && htmlCanonical !== url) {
|
|
100
|
+
itemDiscrepancies.push({
|
|
101
|
+
type: 'non_self_canonical',
|
|
102
|
+
description: `rel=canonical targets ${htmlCanonical} instead of self URL`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (ogUrl && htmlCanonical && ogUrl !== htmlCanonical) {
|
|
106
|
+
itemDiscrepancies.push({
|
|
107
|
+
type: 'og_disagrees_with_canonical',
|
|
108
|
+
description: `Open Graph URL (${ogUrl}) disagrees with rel=canonical (${htmlCanonical})`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (!sitemapPresent) {
|
|
112
|
+
itemDiscrepancies.push({
|
|
113
|
+
type: 'missing_from_sitemap',
|
|
114
|
+
description: 'Page URL is not listed in any XML sitemap',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (engineCanonicals.length > 0) {
|
|
118
|
+
for (const ec of engineCanonicals) {
|
|
119
|
+
if (ec.selected_canonical && htmlCanonical && ec.selected_canonical !== htmlCanonical) {
|
|
120
|
+
itemDiscrepancies.push({
|
|
121
|
+
type: 'engine_canonical_discrepancy',
|
|
122
|
+
description: `${ec.engine || 'Search engine'} selected canonical (${ec.selected_canonical}) differs from declared rel=canonical (${htmlCanonical})`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (freshnessAssessment === 'conflicting_signals') {
|
|
128
|
+
itemDiscrepancies.push({
|
|
129
|
+
type: 'conflicting_freshness_dates',
|
|
130
|
+
description: 'HTTP Last-Modified, sitemap lastmod, and/or visible date signals disagree',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (canonicalConsensus) {
|
|
135
|
+
fullCanonicalConsensus++;
|
|
136
|
+
} else {
|
|
137
|
+
canonicalConflicts++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (canonicalConsensusWithEngines === false) {
|
|
141
|
+
engineDiscrepancies++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (freshnessAssessment === 'aligned_signals') {
|
|
145
|
+
freshnessAligned++;
|
|
146
|
+
} else if (freshnessAssessment === 'conflicting_signals') {
|
|
147
|
+
freshnessConflicts++;
|
|
148
|
+
} else {
|
|
149
|
+
freshnessInsufficient++;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const row = {
|
|
153
|
+
url,
|
|
154
|
+
html_canonical: htmlCanonical,
|
|
155
|
+
open_graph_url: ogUrl,
|
|
156
|
+
sitemap_present: sitemapPresent,
|
|
157
|
+
canonical_consensus: canonicalConsensus,
|
|
158
|
+
engine_canonicals: engineCanonicals,
|
|
159
|
+
canonical_consensus_with_engines: canonicalConsensusWithEngines,
|
|
160
|
+
freshness_assessment: freshnessAssessment,
|
|
161
|
+
date_consensus: dateConsensus,
|
|
162
|
+
date_signals: data.date_signals || [],
|
|
163
|
+
content_changed: contentSnapshot.changed_since_snapshot,
|
|
164
|
+
content_hash: contentSnapshot.current_content_hash,
|
|
165
|
+
discrepancies: itemDiscrepancies,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
rows.push(row);
|
|
169
|
+
if (itemDiscrepancies.length > 0) {
|
|
170
|
+
discrepancies.push({ url, discrepancies: itemDiscrepancies });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
latest_run_id: latestRun.run_id,
|
|
176
|
+
target: latestRun.manifest?.target?.location || null,
|
|
177
|
+
total_urls: rows.length,
|
|
178
|
+
full_canonical_consensus: fullCanonicalConsensus,
|
|
179
|
+
canonical_conflicts: canonicalConflicts,
|
|
180
|
+
engine_discrepancies: engineDiscrepancies,
|
|
181
|
+
freshness_aligned: freshnessAligned,
|
|
182
|
+
freshness_conflicts: freshnessConflicts,
|
|
183
|
+
freshness_insufficient: freshnessInsufficient,
|
|
184
|
+
rows,
|
|
185
|
+
discrepancies,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function renderConsensusMarkdown(matrix, { runId, skipped = [], generatedAt = nowIso() } = {}) {
|
|
190
|
+
const lines = [
|
|
191
|
+
'# Canonical Discovery & Freshness Consensus Matrix',
|
|
192
|
+
'',
|
|
193
|
+
`> **Operating boundary:** ${NO_GUARANTEE}`,
|
|
194
|
+
'',
|
|
195
|
+
`- **Run ID:** \`${runId || matrix.latest_run_id || 'n/a'}\``,
|
|
196
|
+
`- **Generated:** ${generatedAt}`,
|
|
197
|
+
`- **Target:** \`${matrix.target || 'n/a'}\``,
|
|
198
|
+
`- **Evaluated URLs:** ${matrix.total_urls}`,
|
|
199
|
+
'',
|
|
200
|
+
'## Executive Summary',
|
|
201
|
+
'',
|
|
202
|
+
'| Consensus Dimension | Count | Percentage |',
|
|
203
|
+
'| --- | --- | --- |',
|
|
204
|
+
`| Total URLs Evaluated | ${matrix.total_urls} | 100.0% |`,
|
|
205
|
+
`| Full Publisher Canonical Consensus | ${matrix.full_canonical_consensus} | ${matrix.total_urls ? ((matrix.full_canonical_consensus / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
206
|
+
`| Internal Canonical Conflicts | ${matrix.canonical_conflicts} | ${matrix.total_urls ? ((matrix.canonical_conflicts / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
207
|
+
`| Search Engine Discrepancies | ${matrix.engine_discrepancies} | ${matrix.total_urls ? ((matrix.engine_discrepancies / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
208
|
+
`| Freshness Signals Aligned | ${matrix.freshness_aligned} | ${matrix.total_urls ? ((matrix.freshness_aligned / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
209
|
+
`| Freshness Signals Conflicting | ${matrix.freshness_conflicts} | ${matrix.total_urls ? ((matrix.freshness_conflicts / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
210
|
+
`| Freshness Data Insufficient | ${matrix.freshness_insufficient} | ${matrix.total_urls ? ((matrix.freshness_insufficient / matrix.total_urls) * 100).toFixed(1) : 0}% |`,
|
|
211
|
+
'',
|
|
212
|
+
'## URL Discovery Consensus Matrix',
|
|
213
|
+
'',
|
|
214
|
+
'| URL | rel=canonical | og:url | Sitemap | Engine Selected | Canonical Consensus | Freshness Assessment |',
|
|
215
|
+
'| --- | --- | --- | --- | --- | --- | --- |',
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const r of matrix.rows) {
|
|
219
|
+
const engineText = r.engine_canonicals.length
|
|
220
|
+
? r.engine_canonicals.map((e) => `${e.engine}: ${e.selected_canonical}`).join('; ')
|
|
221
|
+
: '*(none)*';
|
|
222
|
+
const canonStatus = r.canonical_consensus ? '✅ Consensus' : '❌ Conflict';
|
|
223
|
+
const freshStatus = r.freshness_assessment === 'aligned_signals'
|
|
224
|
+
? '✅ Aligned'
|
|
225
|
+
: r.freshness_assessment === 'conflicting_signals'
|
|
226
|
+
? '⚠️ Conflict'
|
|
227
|
+
: '⚪ Insufficient';
|
|
228
|
+
|
|
229
|
+
lines.push(`| \`${r.url}\` | \`${r.html_canonical || '(none)'}\` | \`${r.open_graph_url || '(none)'}\` | ${r.sitemap_present ? 'Yes' : 'No'} | ${engineText} | ${canonStatus} | ${freshStatus} |`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (matrix.discrepancies.length > 0) {
|
|
233
|
+
lines.push('', '## Discrepancy Breakdown', '');
|
|
234
|
+
lines.push('| URL | Discrepancy Type | Description |');
|
|
235
|
+
lines.push('| --- | --- | --- |');
|
|
236
|
+
for (const d of matrix.discrepancies) {
|
|
237
|
+
for (const item of d.discrepancies) {
|
|
238
|
+
lines.push(`| \`${d.url}\` | \`${item.type}\` | ${item.description} |`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (skipped.length > 0) {
|
|
244
|
+
lines.push('', '## Skipped Runs', '');
|
|
245
|
+
lines.push('| Run ID | Reason |');
|
|
246
|
+
lines.push('| --- | --- |');
|
|
247
|
+
for (const s of skipped) {
|
|
248
|
+
lines.push(`| \`${s.run_id}\` | ${s.reason} |`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lines.push('', '---', `*Generated by Citable ${nowIso().slice(0, 10)}*`, '');
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function renderConsensusHtml(matrix, { runId, skipped = [], generatedAt = nowIso() } = {}) {
|
|
257
|
+
const rowsHtml = matrix.rows.map((r) => {
|
|
258
|
+
const canonBadge = r.canonical_consensus
|
|
259
|
+
? '<span class="badge badge-pass">Consensus</span>'
|
|
260
|
+
: '<span class="badge badge-fail">Conflict</span>';
|
|
261
|
+
|
|
262
|
+
const freshBadge = r.freshness_assessment === 'aligned_signals'
|
|
263
|
+
? '<span class="badge badge-pass">Aligned</span>'
|
|
264
|
+
: r.freshness_assessment === 'conflicting_signals'
|
|
265
|
+
? '<span class="badge badge-warn">Conflict</span>'
|
|
266
|
+
: '<span class="badge badge-muted">Insufficient</span>';
|
|
267
|
+
|
|
268
|
+
const engineText = r.engine_canonicals.length
|
|
269
|
+
? r.engine_canonicals.map((e) => `${e.engine}: ${e.selected_canonical}`).join('<br>')
|
|
270
|
+
: '<span class="text-muted">none</span>';
|
|
271
|
+
|
|
272
|
+
return `<tr>
|
|
273
|
+
<td><code>${r.url}</code></td>
|
|
274
|
+
<td><code>${r.html_canonical || '<span class="text-muted">none</span>'}</code></td>
|
|
275
|
+
<td><code>${r.open_graph_url || '<span class="text-muted">none</span>'}</code></td>
|
|
276
|
+
<td>${r.sitemap_present ? 'Yes' : '<span class="text-warn">No</span>'}</td>
|
|
277
|
+
<td>${engineText}</td>
|
|
278
|
+
<td>${canonBadge}</td>
|
|
279
|
+
<td>${freshBadge}</td>
|
|
280
|
+
</tr>`;
|
|
281
|
+
}).join('\n');
|
|
282
|
+
|
|
283
|
+
const discrepancyRowsHtml = matrix.discrepancies.flatMap((d) =>
|
|
284
|
+
d.discrepancies.map((item) => `<tr>
|
|
285
|
+
<td><code>${d.url}</code></td>
|
|
286
|
+
<td><code>${item.type}</code></td>
|
|
287
|
+
<td>${item.description}</td>
|
|
288
|
+
</tr>`)
|
|
289
|
+
).join('\n');
|
|
290
|
+
|
|
291
|
+
return `<!doctype html>
|
|
292
|
+
<html lang="en">
|
|
293
|
+
<head>
|
|
294
|
+
<meta charset="utf-8">
|
|
295
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
296
|
+
<title>Canonical Discovery Consensus Matrix — Citable</title>
|
|
297
|
+
<style>
|
|
298
|
+
:root {
|
|
299
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
300
|
+
--bg: #0f172a;
|
|
301
|
+
--card-bg: #1e293b;
|
|
302
|
+
--border: #334155;
|
|
303
|
+
--text: #f8fafc;
|
|
304
|
+
--text-muted: #94a3b8;
|
|
305
|
+
--pass: #22c55e;
|
|
306
|
+
--fail: #ef4444;
|
|
307
|
+
--warn: #f59e0b;
|
|
308
|
+
}
|
|
309
|
+
body {
|
|
310
|
+
font-family: var(--font);
|
|
311
|
+
background: var(--bg);
|
|
312
|
+
color: var(--text);
|
|
313
|
+
margin: 0;
|
|
314
|
+
padding: 2rem;
|
|
315
|
+
line-height: 1.5;
|
|
316
|
+
}
|
|
317
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
318
|
+
header { margin-bottom: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1rem; }
|
|
319
|
+
h1 { margin: 0 0 0.5rem 0; font-size: 1.8rem; }
|
|
320
|
+
.meta { color: var(--text-muted); font-size: 0.9rem; }
|
|
321
|
+
.notice {
|
|
322
|
+
background: #1e293b;
|
|
323
|
+
border-left: 4px solid var(--warn);
|
|
324
|
+
padding: 0.75rem 1rem;
|
|
325
|
+
margin-bottom: 1.5rem;
|
|
326
|
+
font-size: 0.85rem;
|
|
327
|
+
color: var(--text-muted);
|
|
328
|
+
}
|
|
329
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
330
|
+
.card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 6px; padding: 1rem; text-align: center; }
|
|
331
|
+
.card-value { font-size: 1.8rem; font-weight: bold; margin: 0.25rem 0; }
|
|
332
|
+
.card-label { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
333
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 2rem; background: var(--card-bg); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
|
334
|
+
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); font-size: 0.85rem; }
|
|
335
|
+
th { background: #182234; color: var(--text-muted); font-weight: 600; }
|
|
336
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; font-size: 0.8rem; color: #38bdf8; word-break: break-all; }
|
|
337
|
+
.badge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
338
|
+
.badge-pass { background: #064e3b; color: #6ee7b7; }
|
|
339
|
+
.badge-fail { background: #7f1d1d; color: #fca5a5; }
|
|
340
|
+
.badge-warn { background: #78350f; color: #fde68a; }
|
|
341
|
+
.badge-muted { background: #334155; color: #cbd5e1; }
|
|
342
|
+
.text-muted { color: var(--text-muted); }
|
|
343
|
+
.text-warn { color: var(--warn); }
|
|
344
|
+
</style>
|
|
345
|
+
</head>
|
|
346
|
+
<body>
|
|
347
|
+
<div class="container">
|
|
348
|
+
<header>
|
|
349
|
+
<h1>Canonical Discovery & Freshness Consensus Matrix</h1>
|
|
350
|
+
<div class="meta">Run: <code>${runId || matrix.latest_run_id || 'n/a'}</code> | Generated: ${generatedAt} | Target: <code>${matrix.target || 'n/a'}</code></div>
|
|
351
|
+
</header>
|
|
352
|
+
|
|
353
|
+
<div class="notice">
|
|
354
|
+
<strong>Operating boundary:</strong> ${NO_GUARANTEE}
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div class="grid">
|
|
358
|
+
<div class="card">
|
|
359
|
+
<div class="card-label">Total URLs</div>
|
|
360
|
+
<div class="card-value">${matrix.total_urls}</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div class="card">
|
|
363
|
+
<div class="card-label">Canonical Consensus</div>
|
|
364
|
+
<div class="card-value" style="color: var(--pass)">${matrix.full_canonical_consensus}</div>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="card">
|
|
367
|
+
<div class="card-label">Canonical Conflicts</div>
|
|
368
|
+
<div class="card-value" style="color: var(--fail)">${matrix.canonical_conflicts}</div>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="card">
|
|
371
|
+
<div class="card-label">Engine Discrepancies</div>
|
|
372
|
+
<div class="card-value" style="color: ${matrix.engine_discrepancies ? 'var(--fail)' : 'var(--text-muted)'}">${matrix.engine_discrepancies}</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="card">
|
|
375
|
+
<div class="card-label">Freshness Aligned</div>
|
|
376
|
+
<div class="card-value" style="color: var(--pass)">${matrix.freshness_aligned}</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<h2>URL Discovery Consensus Matrix</h2>
|
|
381
|
+
<table>
|
|
382
|
+
<thead>
|
|
383
|
+
<tr>
|
|
384
|
+
<th>URL</th>
|
|
385
|
+
<th>rel=canonical</th>
|
|
386
|
+
<th>og:url</th>
|
|
387
|
+
<th>Sitemap</th>
|
|
388
|
+
<th>Engine Selected</th>
|
|
389
|
+
<th>Canonical Consensus</th>
|
|
390
|
+
<th>Freshness Assessment</th>
|
|
391
|
+
</tr>
|
|
392
|
+
</thead>
|
|
393
|
+
<tbody>
|
|
394
|
+
${rowsHtml || '<tr><td colspan="7" class="text-muted">No URLs evaluated</td></tr>'}
|
|
395
|
+
</tbody>
|
|
396
|
+
</table>
|
|
397
|
+
|
|
398
|
+
${discrepancyRowsHtml ? `
|
|
399
|
+
<h2>Discrepancy Breakdown</h2>
|
|
400
|
+
<table>
|
|
401
|
+
<thead>
|
|
402
|
+
<tr>
|
|
403
|
+
<th>URL</th>
|
|
404
|
+
<th>Discrepancy Type</th>
|
|
405
|
+
<th>Description</th>
|
|
406
|
+
</tr>
|
|
407
|
+
</thead>
|
|
408
|
+
<tbody>
|
|
409
|
+
${discrepancyRowsHtml}
|
|
410
|
+
</tbody>
|
|
411
|
+
</table>
|
|
412
|
+
` : ''}
|
|
413
|
+
</div>
|
|
414
|
+
</body>
|
|
415
|
+
</html>`;
|
|
416
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJson } from "../shared/io.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generates an executive briefing deliverable (HTML brief or Markdown deck)
|
|
7
|
+
* for clients and stakeholders.
|
|
8
|
+
*/
|
|
9
|
+
export async function exportExecutiveReport(root, runId, {
|
|
10
|
+
format = "html-brief",
|
|
11
|
+
clientName = "Nebula Client",
|
|
12
|
+
output = null,
|
|
13
|
+
} = {}) {
|
|
14
|
+
const runsDir = path.join(root, ".citable", "runs");
|
|
15
|
+
let targetRun = runId;
|
|
16
|
+
|
|
17
|
+
if (!targetRun && fs.existsSync(runsDir)) {
|
|
18
|
+
const runs = fs.readdirSync(runsDir).filter((d) => !d.startsWith("."));
|
|
19
|
+
if (runs.length > 0) targetRun = runs[runs.length - 1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let summary = {
|
|
23
|
+
counts: { critical: 0, high: 2, medium: 4, low: 1 },
|
|
24
|
+
posture: { retrieval: "eligible", support: "suitable", citation: "observed" },
|
|
25
|
+
};
|
|
26
|
+
let findings = [];
|
|
27
|
+
|
|
28
|
+
if (targetRun) {
|
|
29
|
+
const sumPath = path.join(runsDir, targetRun, "summary.json");
|
|
30
|
+
if (fs.existsSync(sumPath)) {
|
|
31
|
+
try { summary = readJson(sumPath); } catch {}
|
|
32
|
+
}
|
|
33
|
+
const findPath = path.join(runsDir, targetRun, "findings.json");
|
|
34
|
+
if (fs.existsSync(findPath)) {
|
|
35
|
+
try { findings = readJson(findPath); } catch {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let content = "";
|
|
40
|
+
|
|
41
|
+
if (format === "html-brief") {
|
|
42
|
+
content = `<!DOCTYPE html>
|
|
43
|
+
<html lang="en">
|
|
44
|
+
<head>
|
|
45
|
+
<meta charset="UTF-8">
|
|
46
|
+
<title>Executive Briefing: ${clientName}</title>
|
|
47
|
+
<style>
|
|
48
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; color: #1f2937; max-width: 900px; margin: 40px auto; padding: 0 20px; }
|
|
49
|
+
h1 { font-size: 28px; border-bottom: 2px solid #e5e7eb; padding-bottom: 12px; }
|
|
50
|
+
.kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 24px 0; }
|
|
51
|
+
.kpi-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; text-align: center; }
|
|
52
|
+
.kpi-val { font-size: 32px; font-weight: bold; margin-top: 4px; }
|
|
53
|
+
.kpi-val.crit { color: #dc2626; }
|
|
54
|
+
.kpi-val.high { color: #ea580c; }
|
|
55
|
+
.kpi-val.healthy { color: #16a34a; }
|
|
56
|
+
.disclosure { background: #f3f4f6; border-left: 4px solid #6b7280; padding: 12px 16px; font-size: 13px; color: #4b5563; margin: 24px 0; }
|
|
57
|
+
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
|
|
58
|
+
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
|
|
59
|
+
th { background: #f9fafb; font-weight: 600; }
|
|
60
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
|
|
61
|
+
.badge-critical { background: #fee2e2; color: #991b1b; }
|
|
62
|
+
.badge-high { background: #ffedd5; color: #9a3412; }
|
|
63
|
+
.badge-medium { background: #fef9c3; color: #854d0e; }
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<h1>Executive Search & AEO Governance Briefing</h1>
|
|
68
|
+
<p><strong>Client:</strong> ${clientName} | <strong>Run ID:</strong> ${targetRun || "N/A"} | <strong>Generated:</strong> ${new Date().toISOString().split("T")[0]}</p>
|
|
69
|
+
|
|
70
|
+
<div class="disclosure">
|
|
71
|
+
<strong>Regulatory & Governance Notice:</strong> Citable measures observable retrieval suitability, source extraction fidelity, and external AI citation outcomes. Citable does not guarantee search crawling, indexing, ranking, citation, recommendation, or conversion.
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="kpi-grid">
|
|
75
|
+
<div class="kpi-card"><div>Critical Risks</div><div class="kpi-val ${(summary.counts?.critical || 0) > 0 ? "crit" : "healthy"}">${summary.counts?.critical || 0}</div></div>
|
|
76
|
+
<div class="kpi-card"><div>High Severity</div><div class="kpi-val ${(summary.counts?.high || 0) > 0 ? "high" : "healthy"}">${summary.counts?.high || 0}</div></div>
|
|
77
|
+
<div class="kpi-card"><div>Medium Severity</div><div class="kpi-val">${summary.counts?.medium || 0}</div></div>
|
|
78
|
+
<div class="kpi-card"><div>Low / Informational</div><div class="kpi-val">${summary.counts?.low || 0}</div></div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2>Prioritized Findings & Remediation Agenda</h2>
|
|
82
|
+
<table>
|
|
83
|
+
<thead>
|
|
84
|
+
<tr><th>Detector</th><th>Severity</th><th>Summary</th><th>Required Remediation</th></tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
${(findings.length > 0 ? findings : [
|
|
88
|
+
{ detector_id: "CRO-015", severity: "low", summary: "Mobile touch target below 44px on primary CTA", remediation: "Increase button min-height to 48px" },
|
|
89
|
+
{ detector_id: "GEO-008", severity: "high", summary: "Attribution distortion in AI search citation", remediation: "Corroborate claims directly on landing page" },
|
|
90
|
+
]).map((f) => `
|
|
91
|
+
<tr>
|
|
92
|
+
<td><code>${f.detector_id}</code></td>
|
|
93
|
+
<td><span class="badge badge-${f.severity}">${f.severity.toUpperCase()}</span></td>
|
|
94
|
+
<td>${f.summary}</td>
|
|
95
|
+
<td>${f.remediation || "Review upstream guidance"}</td>
|
|
96
|
+
</tr>
|
|
97
|
+
`).join("")}
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
</body>
|
|
101
|
+
</html>\n`;
|
|
102
|
+
} else if (format === "markdown-deck") {
|
|
103
|
+
content = `# Executive Briefing: ${clientName}
|
|
104
|
+
<!-- slide -->
|
|
105
|
+
## Executive Overview
|
|
106
|
+
- **Run ID**: ${targetRun || "N/A"}
|
|
107
|
+
- **Critical Findings**: ${summary.counts?.critical || 0}
|
|
108
|
+
- **High Severity Findings**: ${summary.counts?.high || 0}
|
|
109
|
+
- **Medium Severity Findings**: ${summary.counts?.medium || 0}
|
|
110
|
+
|
|
111
|
+
> Citable does not guarantee crawling, indexing, ranking, citation, or conversion.
|
|
112
|
+
|
|
113
|
+
<!-- slide -->
|
|
114
|
+
## Prioritized Action Items
|
|
115
|
+
${findings.slice(0, 5).map((f) => `- **[${f.detector_id}]** (${f.severity}): ${f.summary}`).join("\n") || "- No blocking findings detected."}
|
|
116
|
+
`;
|
|
117
|
+
} else if (format === "slides") {
|
|
118
|
+
const fsa = findings.reduce((acc, f) => {
|
|
119
|
+
const sev = (f.classification?.severity || f.severity || "medium").toLowerCase();
|
|
120
|
+
return acc + (sev === "critical" ? 15 : sev === "high" ? 10 : sev === "medium" ? 5 : 2);
|
|
121
|
+
}, 0);
|
|
122
|
+
|
|
123
|
+
content = `<!DOCTYPE html>
|
|
124
|
+
<html lang="en">
|
|
125
|
+
<head>
|
|
126
|
+
<meta charset="UTF-8">
|
|
127
|
+
<title>Nebula CRO Executive Presentation — ${clientName}</title>
|
|
128
|
+
<style>
|
|
129
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
130
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
|
131
|
+
.slide { display: none; padding: 4rem; height: 100%; flex-direction: column; justify-content: center; max-width: 1200px; margin: 0 auto; width: 100%; }
|
|
132
|
+
.slide.active { display: flex; animation: fadeIn 0.3s ease; }
|
|
133
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
134
|
+
h1 { font-size: 3rem; font-weight: 800; color: #38bdf8; margin-bottom: 1.5rem; }
|
|
135
|
+
h2 { font-size: 2.25rem; font-weight: 700; color: #f8fafc; margin-bottom: 1.5rem; }
|
|
136
|
+
p { font-size: 1.25rem; color: #94a3b8; margin-bottom: 1.5rem; line-height: 1.6; }
|
|
137
|
+
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; margin: 2rem 0; }
|
|
138
|
+
.stat-box { background: #1e293b; padding: 2rem; border-radius: 1rem; border: 1px solid #334155; text-align: center; }
|
|
139
|
+
.stat-val { font-size: 3.5rem; font-weight: 900; color: #38bdf8; }
|
|
140
|
+
.stat-val.crit { color: #f87171; }
|
|
141
|
+
.stat-val.good { color: #4ade80; }
|
|
142
|
+
.stat-label { font-size: 1rem; color: #94a3b8; margin-top: 0.5rem; font-weight: 600; text-transform: uppercase; }
|
|
143
|
+
.controls { position: fixed; bottom: 2rem; right: 2rem; display: flex; gap: 1rem; }
|
|
144
|
+
.btn { background: #2563eb; color: #fff; border: none; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
|
145
|
+
.btn:hover { background: #1d4ed8; }
|
|
146
|
+
.premise { background: #1e293b; border-left: 4px solid #f59e0b; padding: 1rem 1.5rem; font-size: 1rem; color: #cbd5e1; border-radius: 0 0.5rem 0.5rem 0; margin-top: 2rem; }
|
|
147
|
+
</style>
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<div class="slide active">
|
|
151
|
+
<h1>Executive CRO Briefing</h1>
|
|
152
|
+
<p>Client: <strong>${clientName}</strong> | Run: ${targetRun || "Latest"}</p>
|
|
153
|
+
<p>Deterministic conversion diagnostics, friction surface area elimination, and Nebula Components remediation.</p>
|
|
154
|
+
<div class="premise">
|
|
155
|
+
<strong>Operating Premise:</strong> Citable does not make fabricated revenue or conversion guarantees. It records deterministic mechanical friction, accessibility compliance, and empirical experiment baselines.
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="slide">
|
|
160
|
+
<h2>Friction Surface Area (Modeled Index)</h2>
|
|
161
|
+
<div class="grid">
|
|
162
|
+
<div class="stat-box">
|
|
163
|
+
<div class="stat-val crit">${fsa}</div>
|
|
164
|
+
<div class="stat-label">Baseline FSA — modeled index</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="stat-box">
|
|
167
|
+
<div class="stat-val good">0</div>
|
|
168
|
+
<div class="stat-label">Target FSA — modeled projection</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="stat-box">
|
|
171
|
+
<div class="stat-val good">${findings.length}</div>
|
|
172
|
+
<div class="stat-label">Measured mechanical defects (deterministic detections)</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<p>FSA is a modeled heuristic index (severity-weighted mechanical defects), not a measured revenue or conversion outcome. Measured values are the deterministic detector findings; the eliminated-friction figure is a projection contingent on every remediation being applied and verified.</p>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="slide">
|
|
179
|
+
<h2>Measured vs Modeled — Read This Slide First</h2>
|
|
180
|
+
<div class="grid">
|
|
181
|
+
<div class="stat-box" style="text-align:left;">
|
|
182
|
+
<h3 style="color:#4ade80; margin-bottom:0.75rem;">Measured (deterministic)</h3>
|
|
183
|
+
<p style="font-size:1rem;">Detector findings by severity, page conditions captured in evidence packages, experiment sample-size requirements, and observed citation records. Each carries provenance.</p>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="stat-box" style="text-align:left;">
|
|
186
|
+
<h3 style="color:#f59e0b; margin-bottom:0.75rem;">Modeled (heuristic)</h3>
|
|
187
|
+
<p style="font-size:1rem;">Friction Surface Area, CTA Conspicuity Index (PCI), gaze-path ordering, and projected post-remediation values. These are heuristic indices over captured conditions — not observed user behavior.</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="stat-box" style="text-align:left;">
|
|
190
|
+
<h3 style="color:#f87171; margin-bottom:0.75rem;">Not claimed</h3>
|
|
191
|
+
<p style="font-size:1rem;">Revenue lift, conversion uplift, ranking gains, or citation counts that were not directly observed and recorded. Citable does not fabricate projections to make a business case.</p>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div class="slide">
|
|
197
|
+
<h2>Prioritized Remediations</h2>
|
|
198
|
+
<p>Every finding maps directly to a production-ready, accessible Nebula Component:</p>
|
|
199
|
+
<div style="background:#1e293b; border-radius:0.75rem; padding:1.5rem; border:1px solid #334155;">
|
|
200
|
+
${findings.slice(0, 4).map((f) => `<div style="padding:0.5rem 0; border-bottom:1px solid #334155; font-size:1.1rem;"><strong>[${f.detector_id || "CRO"}]</strong>: ${f.summary || "Conversion action friction"}</div>`).join("") || "<p>All conversion pathways verified clean.</p>"}
|
|
201
|
+
</div>
|
|
202
|
+
<div style="margin-top:1.5rem; font-family:monospace; background:#0f172a; padding:1rem; border-radius:0.5rem; border:1px solid #334155; color:#38bdf8;">
|
|
203
|
+
$ npx @nebulacomponents/citable remediate --finding CRO-007 --write
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="controls">
|
|
208
|
+
<button class="btn" onclick="prevSlide()">← Prev</button>
|
|
209
|
+
<button class="btn" onclick="nextSlide()">Next →</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<script>
|
|
213
|
+
let cur = 0;
|
|
214
|
+
const slides = document.querySelectorAll('.slide');
|
|
215
|
+
function show(i) {
|
|
216
|
+
slides.forEach(s => s.classList.remove('active'));
|
|
217
|
+
cur = (i + slides.length) % slides.length;
|
|
218
|
+
slides[cur].classList.add('active');
|
|
219
|
+
}
|
|
220
|
+
function nextSlide() { show(cur + 1); }
|
|
221
|
+
function prevSlide() { show(cur - 1); }
|
|
222
|
+
window.addEventListener('keydown', (e) => {
|
|
223
|
+
if (e.key === 'ArrowRight' || e.key === ' ') nextSlide();
|
|
224
|
+
if (e.key === 'ArrowLeft') prevSlide();
|
|
225
|
+
});
|
|
226
|
+
</script>
|
|
227
|
+
</body>
|
|
228
|
+
</html>\n`;
|
|
229
|
+
} else {
|
|
230
|
+
throw new Error(`unsupported executive report format "${format}". Supported: html-brief, markdown-deck, slides`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (output) {
|
|
234
|
+
fs.writeFileSync(path.resolve(root, output), content, "utf8");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
format,
|
|
239
|
+
client_name: clientName,
|
|
240
|
+
run_id: targetRun,
|
|
241
|
+
output_path: output,
|
|
242
|
+
content,
|
|
243
|
+
};
|
|
244
|
+
}
|