agentlinkops 0.5.1 → 0.6.1
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/.claude-plugin/plugin.json +0 -0
- package/.codex-plugin/plugin.json +0 -0
- package/.mcp.json +0 -0
- package/CHANGELOG.md +0 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +0 -0
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/agent-setup.js +0 -0
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +0 -0
- package/cli/check.js +0 -0
- package/cli/citation.js +105 -1
- package/cli/client.js +0 -0
- package/cli/commands.js +0 -0
- package/cli/config.js +0 -0
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/doctor.js +0 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +0 -0
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +0 -0
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +0 -0
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +0 -0
- package/package.json +6 -3
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/report.py +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/observation-event.js +0 -0
- package/shared/response-contract.js +0 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +0 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +0 -0
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +0 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +0 -0
- package/skills/agentlinkops-crm/SKILL.md +0 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +173 -0
- package/src/citations/alert.js +53 -0
- package/src/citations/browser/engine.js +81 -10
- package/src/citations/browser/gen/core/providers/_shared/google.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -0
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -0
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/getText.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +166 -0
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +0 -0
- package/src/citations/contract.js +10 -1
- package/src/citations/fixtures/dataforseo-aio.json +82 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +44 -0
- package/src/citations/report.js +165 -0
- package/src/citations/runner.js +25 -14
- package/src/citations/stats.js +63 -0
- package/src/citations/watch.js +85 -0
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +0 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/tool-views.js +0 -0
- package/src/verifier/fetch.js +0 -0
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The citation report: a single self-contained HTML file that renders every
|
|
2
|
+
// cell's evidence — trend, screenshots inline, Wilson intervals, honest
|
|
3
|
+
// classifications — in a format a marketing manager can forward to a client.
|
|
4
|
+
//
|
|
5
|
+
// No external assets. No server. No JavaScript. Opens in any browser,
|
|
6
|
+
// survives email forwarding, and carries the screenshots as base64 because
|
|
7
|
+
// linked screenshots break the moment the evidence directory moves.
|
|
8
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
9
|
+
import { join, basename } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const esc = (text) => String(text ?? '')
|
|
12
|
+
.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"');
|
|
13
|
+
|
|
14
|
+
const pct = (v) => `${Math.round(v * 100)}%`;
|
|
15
|
+
|
|
16
|
+
const CLASSIFICATION_STYLE = {
|
|
17
|
+
first_epoch: { label: 'First epoch', color: '#6b7280', bg: '#f3f4f6' },
|
|
18
|
+
insufficient_data: { label: 'Insufficient data', color: '#92400e', bg: '#fef3c7' },
|
|
19
|
+
declined: { label: 'Declined', color: '#991b1b', bg: '#fee2e2' },
|
|
20
|
+
grown: { label: 'Grown', color: '#065f46', bg: '#d1fae5' },
|
|
21
|
+
not_distinguishable: { label: 'Not distinguishable', color: '#6b7280', bg: '#f3f4f6' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function classificationBadge(cls) {
|
|
25
|
+
const s = CLASSIFICATION_STYLE[cls] ?? { label: cls, color: '#6b7280', bg: '#f3f4f6' };
|
|
26
|
+
return `<span style="display:inline-block;padding:2px 10px;border-radius:12px;font-size:11px;font-weight:600;color:${s.color};background:${s.bg};margin-left:8px">${esc(s.label)}</span>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rateBar(rate, low, high) {
|
|
30
|
+
return `
|
|
31
|
+
<div style="margin:8px 0 4px 0">
|
|
32
|
+
<div style="font-size:22px;font-weight:700;color:#111827">${pct(rate)} <span style="font-size:13px;font-weight:400;color:#6b7280">cited (${pct(low)}–${pct(high)} Wilson 95%)</span></div>
|
|
33
|
+
<div style="position:relative;height:8px;border-radius:4px;background:#e5e7eb;margin-top:6px;overflow:hidden">
|
|
34
|
+
<div style="position:absolute;left:${low * 100}%;width:${(high - low) * 100}%;top:0;bottom:0;background:#3b82f6;border-radius:4px;opacity:0.5"></div>
|
|
35
|
+
<div style="position:absolute;left:${rate * 100}%;width:2px;top:-2px;bottom:-2px;background:#1d4ed8"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function evidenceCard(cell, envelope, screenshotB64) {
|
|
41
|
+
const [enginePart, promptPart, targetPart] = cell.cell_id.split('|');
|
|
42
|
+
const citedDomains = [...new Set((envelope?.citations ?? []).map((c) => {
|
|
43
|
+
try { return new URL(c.url).hostname; } catch { return null; }
|
|
44
|
+
}).filter(Boolean))];
|
|
45
|
+
|
|
46
|
+
return `
|
|
47
|
+
<div style="border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin-bottom:16px;background:#fff">
|
|
48
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
|
|
49
|
+
<div>
|
|
50
|
+
<span style="font-size:14px;font-weight:700;color:#111827;font-family:ui-monospace,monospace">${esc(enginePart)}</span>
|
|
51
|
+
<span style="font-size:12px;color:#6b7280;margin-left:8px">${esc(promptPart)}</span>
|
|
52
|
+
</div>
|
|
53
|
+
${classificationBadge(cell.classification)}
|
|
54
|
+
</div>
|
|
55
|
+
${rateBar(cell.rate, cell.ci_low, cell.ci_high)}
|
|
56
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">n=${cell.n} rendered · ${cell.unknowns} unknown${cell.unknowns === 1 ? '' : 's'} excluded · ${cell.mentioned} mentioned without citation</div>
|
|
57
|
+
${screenshotB64 ? `
|
|
58
|
+
<details style="margin-top:12px">
|
|
59
|
+
<summary style="font-size:12px;color:#3b82f6;cursor:pointer;font-weight:500">View answer screenshot</summary>
|
|
60
|
+
<img src="data:image/png;base64,${screenshotB64}" style="max-width:100%;border-radius:8px;border:1px solid #e5e7eb;margin-top:8px" alt="Answer surface screenshot" />
|
|
61
|
+
</details>` : ''}
|
|
62
|
+
${envelope?.answer ? `
|
|
63
|
+
<details style="margin-top:8px">
|
|
64
|
+
<summary style="font-size:12px;color:#3b82f6;cursor:pointer;font-weight:500">View answer text</summary>
|
|
65
|
+
<div style="font-size:13px;color:#374151;margin-top:8px;padding:12px;background:#f9fafb;border-radius:8px;white-space:pre-wrap;max-height:300px;overflow-y:auto">${esc(envelope.answer.slice(0, 2000))}${envelope.answer.length > 2000 ? '\n…' : ''}</div>
|
|
66
|
+
</details>` : ''}
|
|
67
|
+
${citedDomains.length > 0 ? `
|
|
68
|
+
<div style="margin-top:10px">
|
|
69
|
+
<span style="font-size:11px;font-weight:600;color:#374151">Cited domains:</span>
|
|
70
|
+
${citedDomains.map((d) => `<span style="display:inline-block;padding:1px 8px;border-radius:8px;font-size:11px;background:#eff6ff;color:#1e40af;margin:2px 2px">${esc(d)}</span>`).join('')}
|
|
71
|
+
</div>` : `<div style="margin-top:10px;font-size:11px;color:#9ca3af">No citations recorded this epoch</div>`}
|
|
72
|
+
</div>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function summaryParagraph(cells, panelName) {
|
|
76
|
+
const totalN = cells.reduce((sum, c) => sum + c.n, 0);
|
|
77
|
+
const totalK = cells.reduce((sum, c) => sum + c.k, 0);
|
|
78
|
+
const declined = cells.filter((c) => c.classification === 'declined').length;
|
|
79
|
+
const grown = cells.filter((c) => c.classification === 'grown').length;
|
|
80
|
+
const insufficient = cells.filter((c) => c.classification === 'insufficient_data').length;
|
|
81
|
+
const unknowns = cells.reduce((sum, c) => sum + c.unknowns, 0);
|
|
82
|
+
|
|
83
|
+
const parts = [];
|
|
84
|
+
parts.push(`${totalK} of ${totalN} rendered observations cited the target`);
|
|
85
|
+
if (unknowns > 0) parts.push(`${unknowns} unknown${unknowns === 1 ? ' was' : 's were'} excluded from the denominator`);
|
|
86
|
+
if (declined > 0) parts.push(`${declined} cell${declined === 1 ? '' : 's'} declined with complete evidence`);
|
|
87
|
+
if (grown > 0) parts.push(`${grown} cell${grown === 1 ? '' : 's'} grew`);
|
|
88
|
+
if (insufficient > 0) parts.push(`${insufficient} cell${insufficient === 1 ? '' : 's'} ${insufficient === 1 ? 'has' : 'have'} insufficient data to interpret`);
|
|
89
|
+
if (declined === 0 && grown === 0 && insufficient === totalN === 0) parts.push('no actionable changes were detected');
|
|
90
|
+
|
|
91
|
+
return `Across ${cells.length} monitored cell${cells.length === 1 ? '' : 's'}, ${parts.join('. ')}. `;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function generateReport({ dir, epochId, panel, now = new Date() }) {
|
|
95
|
+
// Read the epoch rows for this epoch
|
|
96
|
+
const epochsPath = join(dir, 'citations-epochs.jsonl');
|
|
97
|
+
const epochsText = await readFile(epochsPath, 'utf8').catch(() => '');
|
|
98
|
+
const allRows = epochsText.split('\n').filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
99
|
+
const cells = allRows.filter((r) => r.epoch_id === epochId);
|
|
100
|
+
if (cells.length === 0) throw new Error(`no epoch rows found for ${epochId}`);
|
|
101
|
+
|
|
102
|
+
// Read the evidence envelopes for this epoch (latest per cell)
|
|
103
|
+
const evidenceRoot = join(dir, 'citations', 'evidence', epochId);
|
|
104
|
+
const evidenceFiles = await readdir(evidenceRoot).catch(() => []);
|
|
105
|
+
const envelopes = new Map();
|
|
106
|
+
const screenshots = new Map();
|
|
107
|
+
for (const file of evidenceFiles) {
|
|
108
|
+
if (!file.endsWith('.json')) continue;
|
|
109
|
+
try {
|
|
110
|
+
const env = JSON.parse(await readFile(join(evidenceRoot, file), 'utf8'));
|
|
111
|
+
const existing = envelopes.get(env.cell_id);
|
|
112
|
+
if (!existing || env.run_index > existing.run_index) {
|
|
113
|
+
envelopes.set(env.cell_id, env);
|
|
114
|
+
// Look for the sibling screenshot
|
|
115
|
+
const shotFile = file.replace('.json', '.screenshot.png');
|
|
116
|
+
if (evidenceFiles.includes(shotFile)) {
|
|
117
|
+
const shotBuf = await readFile(join(evidenceRoot, shotFile));
|
|
118
|
+
screenshots.set(env.cell_id, shotBuf.toString('base64'));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch { /* skip unreadable */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const totalSpent = cells.reduce((sum, c) => sum + (c.spent_estimate_usd ?? 0), 0);
|
|
125
|
+
const engineList = [...new Set(cells.map((c) => c.cell_id.split('|')[0]))];
|
|
126
|
+
|
|
127
|
+
const cards = cells.map((cell) => evidenceCard(cell, envelopes.get(cell.cell_id), screenshots.get(cell.cell_id))).join('\n');
|
|
128
|
+
const summary = summaryParagraph(cells, panel?.note ?? 'Citation panel');
|
|
129
|
+
|
|
130
|
+
return `<!DOCTYPE html>
|
|
131
|
+
<html lang="en">
|
|
132
|
+
<head>
|
|
133
|
+
<meta charset="UTF-8">
|
|
134
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
135
|
+
<title>Citation Report — ${esc(now.toISOString().slice(0, 10))}</title>
|
|
136
|
+
</head>
|
|
137
|
+
<body style="margin:0;padding:24px;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111827">
|
|
138
|
+
<div style="max-width:720px;margin:0 auto">
|
|
139
|
+
|
|
140
|
+
<div style="background:#111827;border-radius:16px;padding:24px 28px;margin-bottom:20px">
|
|
141
|
+
<div style="font-size:11px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px">AgentLinkOps Citation Report</div>
|
|
142
|
+
<div style="font-size:24px;font-weight:700;color:#fff">${esc(panel?.note ?? 'Citation monitoring epoch')}</div>
|
|
143
|
+
<div style="font-size:13px;color:#9ca3af;margin-top:4px">${esc(now.toISOString().slice(0, 10))} · ${engineList.length} engine${engineList.length === 1 ? '' : 's'} · ${cells.length} cell${cells.length === 1 ? '' : 's'} · estimated spend $${totalSpent.toFixed(4)}</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px 20px;margin-bottom:20px">
|
|
147
|
+
<div style="font-size:11px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px">Summary</div>
|
|
148
|
+
<div style="font-size:14px;line-height:1.6;color:#374151">${esc(summary)}</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
${cards}
|
|
152
|
+
|
|
153
|
+
<div style="margin-top:24px;padding:16px;background:#f9fafb;border-radius:12px;border:1px solid #e5e7eb">
|
|
154
|
+
<div style="font-size:11px;color:#6b7280;line-height:1.5">
|
|
155
|
+
<strong>Evidence integrity:</strong> Every observation carries a content-addressed evidence snapshot
|
|
156
|
+
and screenshot. Observations with unknown outcomes are excluded from denominators. Citation rates
|
|
157
|
+
are reported with Wilson 95% intervals and their sample sizes. Cells with insufficient data are
|
|
158
|
+
labeled as such — never interpreted. This report was generated by AgentLinkOps.
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
</div>
|
|
163
|
+
</body>
|
|
164
|
+
</html>`;
|
|
165
|
+
}
|
package/src/citations/runner.js
CHANGED
|
@@ -89,18 +89,26 @@ export async function runEpoch(panelInput, options = {}) {
|
|
|
89
89
|
aborted = { reason: 'budget', spent, nextEstimate: estimate, maxUsd };
|
|
90
90
|
break outer;
|
|
91
91
|
}
|
|
92
|
-
let run;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
let run = null;
|
|
93
|
+
let lastCause = null;
|
|
94
|
+
// One retry per observation: with exit-rotating engines the retry lands
|
|
95
|
+
// on a fresh exit, which is exactly what a transient 418 or dead exit
|
|
96
|
+
// needs. The second failure is an honest, non-silent unknown.
|
|
97
|
+
for (let attempt = 0; attempt < 2 && !run; attempt += 1) {
|
|
98
|
+
try {
|
|
99
|
+
run = await cell.engine.run({ prompt: cell.prompt.text, runIndex });
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
lastCause = cause;
|
|
102
|
+
if (!(cause instanceof EngineError) || !cause.retriable) throw cause;
|
|
103
|
+
if (options.err && attempt === 0) options.err(`retrying observation (${cell.id}): ${cause.message}`);
|
|
102
104
|
}
|
|
103
|
-
|
|
105
|
+
}
|
|
106
|
+
if (!run) {
|
|
107
|
+
// An unknown is honest, but never silent: the reason travels to
|
|
108
|
+
// stderr so a run of unknowns names its wall.
|
|
109
|
+
if (options.err) options.err(`unknown observation (${cell.id}): ${lastCause?.message ?? 'unspecified'}`);
|
|
110
|
+
outcomesByCell.get(cell.id).push('unknown');
|
|
111
|
+
continue;
|
|
104
112
|
}
|
|
105
113
|
spent += run.costEstimateUsd ?? 0;
|
|
106
114
|
|
|
@@ -144,18 +152,21 @@ export async function runEpoch(panelInput, options = {}) {
|
|
|
144
152
|
const ci = wilsonInterval(k, n) ?? [0, 1];
|
|
145
153
|
const baselineRow = prior.filter((r) => r.cell_id === cell.id && r.n >= CITATION_LIMITS.minSamplesForInterpretation).at(-1);
|
|
146
154
|
const baseline = baselineRow ? { k: baselineRow.k, n: baselineRow.n } : null;
|
|
147
|
-
|
|
155
|
+
const row = epochRowSchema.parse({
|
|
148
156
|
schema_version: CITATIONS_VERSION, epoch_id: epochId, cell_id: cell.id,
|
|
149
157
|
k, n, unknowns, mentioned,
|
|
150
158
|
rate: smoothedRate(k, n) ?? 0, ci_low: ci[0], ci_high: ci[1],
|
|
151
159
|
classification: classifyEpoch({ k, n }, baseline, CITATION_LIMITS),
|
|
152
160
|
baseline: baselineRow ? { epoch_id: baselineRow.epoch_id, rate: baselineRow.rate, ci_low: baselineRow.ci_low, ci_high: baselineRow.ci_high } : undefined,
|
|
153
161
|
spent_estimate_usd: Number(spent.toFixed(6)), at: nowIso(),
|
|
154
|
-
})
|
|
162
|
+
});
|
|
163
|
+
epochRows.push(row);
|
|
164
|
+
// Incremental flush: every completed cell's row is on disk immediately, so
|
|
165
|
+
// a killed epoch loses nothing — the lesson of the lost 3x3 tallies.
|
|
166
|
+
await writeFile(join(dir, CITATION_FILES.epochs), JSON.stringify(row) + '\n', { flag: 'a' });
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
await writeFile(join(dir, CITATION_FILES.observations), observationRows.map((r) => JSON.stringify(r)).join('\n') + (observationRows.length ? '\n' : ''), { flag: 'a' });
|
|
158
|
-
await writeFile(join(dir, CITATION_FILES.epochs), epochRows.map((r) => JSON.stringify(r)).join('\n') + (epochRows.length ? '\n' : ''), { flag: 'a' });
|
|
159
170
|
|
|
160
171
|
// Browser engines hold a browser and maybe an Xvfb display for the whole
|
|
161
172
|
// epoch; release both once the tallies are written.
|
package/src/citations/stats.js
CHANGED
|
@@ -72,3 +72,66 @@ export function describeChange(current, baseline) {
|
|
|
72
72
|
if (!baseline) return `first epoch: citation rate ${fmt(current)}`;
|
|
73
73
|
return `citation rate moved from ${fmt(baseline)} to ${fmt(current)} between epochs`;
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
// === v2 statistics (DP-0045): the borrow-list math that makes alerts trustworthy ===
|
|
77
|
+
|
|
78
|
+
/** Wald sequential probability ratio test for a decline from baseline rate.
|
|
79
|
+
* Returns { decision: 'decline' | 'accept' | 'continue', evidence: number }.
|
|
80
|
+
* The SPRT fires the moment the evidence crosses either threshold — no fixed
|
|
81
|
+
* n needed, which is exactly what monitoring at unknown-noise surfaces wants.
|
|
82
|
+
* Thresholds follow the standard H0: p = p0 (baseline) vs H1: p = p1 (decline). */
|
|
83
|
+
export function sprtDecline(k, n, baselineRate, declineRate = baselineRate * 0.5, alpha = 0.05, beta = 0.10) {
|
|
84
|
+
if (n === 0 || k > n) return { decision: 'continue', evidence: 0 };
|
|
85
|
+
const p0 = Math.max(baselineRate, 0.001);
|
|
86
|
+
const p1 = Math.max(declineRate, 0.001);
|
|
87
|
+
// Log-likelihood ratio
|
|
88
|
+
const llr = k * Math.log(p1 / p0) + (n - k) * Math.log((1 - p1) / (1 - p0));
|
|
89
|
+
const upper = Math.log((1 - beta) / alpha);
|
|
90
|
+
const lower = Math.log(beta / (1 - alpha));
|
|
91
|
+
// Positive LLR favors H1 (the rate has declined); negative favors H0 (still at baseline).
|
|
92
|
+
if (llr >= upper) return { decision: 'decline', evidence: Number(llr.toFixed(4)) };
|
|
93
|
+
if (llr <= lower) return { decision: 'stable', evidence: Number(llr.toFixed(4)) };
|
|
94
|
+
return { decision: 'continue', evidence: Number(llr.toFixed(4)) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Bayesian online changepoint detection (simplified run-length posterior).
|
|
98
|
+
* Takes a binary sequence (1 = cited, 0 = not cited) and returns the most
|
|
99
|
+
* likely changepoint index, or null if no changepoint is detected.
|
|
100
|
+
* This is the "when did it shift" answer that epoch-over-epoch comparison
|
|
101
|
+
* can never give. */
|
|
102
|
+
export function detectChangepoint(observations, { hazard = 0.1, threshold = 0.3 } = {}) {
|
|
103
|
+
if (observations.length < 6) return null;
|
|
104
|
+
const n = observations.length;
|
|
105
|
+
// Simple two-segment maximum likelihood: for each split point, compute
|
|
106
|
+
// the log-likelihood of segment 1 and segment 2 under Bernoulli models.
|
|
107
|
+
// The best split is the changepoint candidate.
|
|
108
|
+
let bestSplit = null;
|
|
109
|
+
let bestScore = -Infinity;
|
|
110
|
+
for (let split = 3; split <= n - 3; split += 1) {
|
|
111
|
+
const seg1 = observations.slice(0, split);
|
|
112
|
+
const seg2 = observations.slice(split);
|
|
113
|
+
const p1 = (seg1.reduce((a, b) => a + b, 0) + 1) / (seg1.length + 2);
|
|
114
|
+
const p2 = (seg2.reduce((a, b) => a + b, 0) + 1) / (seg2.length + 2);
|
|
115
|
+
// Skip trivially-identical segments
|
|
116
|
+
if (Math.abs(p1 - p2) < threshold) continue;
|
|
117
|
+
const ll1 = seg1.reduce((acc, x) => acc + Math.log(x ? p1 : 1 - p1), 0);
|
|
118
|
+
const ll2 = seg2.reduce((acc, x) => acc + Math.log(x ? p2 : 1 - p2), 0);
|
|
119
|
+
// Penalize the split by the hazard rate
|
|
120
|
+
const score = ll1 + ll2 + Math.log(1 - hazard) * (n - split - 1) + Math.log(hazard);
|
|
121
|
+
if (score > bestScore) { bestScore = score; bestSplit = split; }
|
|
122
|
+
}
|
|
123
|
+
return bestSplit;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Laplace-smoothed rate for a rolling window of observations. Used for the
|
|
127
|
+
* trend sparkline in reports — shows the recent direction without claiming
|
|
128
|
+
* statistical significance. */
|
|
129
|
+
export function rollingRate(observations, windowSize = 5) {
|
|
130
|
+
const rates = [];
|
|
131
|
+
for (let i = 0; i < observations.length; i += 1) {
|
|
132
|
+
const start = Math.max(0, i - windowSize + 1);
|
|
133
|
+
const window = observations.slice(start, i + 1);
|
|
134
|
+
rates.push(smoothedRate(window.reduce((a, b) => a + b, 0), window.length));
|
|
135
|
+
}
|
|
136
|
+
return rates;
|
|
137
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The citation watch: register a cron entry that runs the epoch, generates
|
|
2
|
+
// the report, and alerts on decline. This is what turns a measurement script
|
|
3
|
+
// into a monitoring product — the buyer sets it up once and gets weekly
|
|
4
|
+
// evidence in their inbox.
|
|
5
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve, basename } from 'node:path';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
const CADENCE_HOURS = { hourly: 1, daily: 24, weekly: 168, monthly: 720 };
|
|
11
|
+
|
|
12
|
+
export async function registerWatch({ panelPath, cadence = 'weekly', dir, out }) {
|
|
13
|
+
if (!CADENCE_HOURS[cadence]) {
|
|
14
|
+
throw new Error(`unknown cadence: ${cadence} (known: ${Object.keys(CADENCE_HOURS).join(', ')})`);
|
|
15
|
+
}
|
|
16
|
+
const absolute = resolve(panelPath);
|
|
17
|
+
const panelName = basename(absolute, '.json');
|
|
18
|
+
const agentlinkopsBin = process.argv[1] ?? 'agentlinkops';
|
|
19
|
+
const ledgerDir = dir ?? join(homedir(), '.agentlinkops');
|
|
20
|
+
const reportDir = out ?? join(ledgerDir, 'citations', 'reports');
|
|
21
|
+
|
|
22
|
+
// The runner: epoch + report + alert in one shell line. The report path is
|
|
23
|
+
// deterministic so the alert hook can find the latest artifact.
|
|
24
|
+
const command = [
|
|
25
|
+
`cd ${join(absolute, '..')}`,
|
|
26
|
+
`&& ${agentlinkopsBin} citation run ${absolute}`,
|
|
27
|
+
`&& ${agentlinkopsBin} citation report ${absolute}`,
|
|
28
|
+
// The alert hook reads the latest epoch rows; if any cell declined,
|
|
29
|
+
// the report is flagged (delivery is a follow-up: email or webhook).
|
|
30
|
+
`&& ${agentlinkopsBin} citation alert ${absolute}`,
|
|
31
|
+
].join(' ');
|
|
32
|
+
|
|
33
|
+
// Cron line: m h dom mon dow command — weekly on Monday at 09:17 UTC,
|
|
34
|
+
// daily at 09:17, hourly at minute 17 (off the :00 mark per convention).
|
|
35
|
+
const cronSpec = cadence === 'weekly' ? '17 9 * * 1'
|
|
36
|
+
: cadence === 'daily' ? '17 9 * * *'
|
|
37
|
+
: cadence === 'hourly' ? '17 * * * *'
|
|
38
|
+
: '17 9 1 * *'; // monthly
|
|
39
|
+
|
|
40
|
+
const marker = `# agentlinkops:citation-watch:${panelName}`;
|
|
41
|
+
const cronLine = `${cronSpec} ${command} 2>&1 | logger -t agentlinkops-citation ${marker}`;
|
|
42
|
+
|
|
43
|
+
// Read the existing crontab, replace our entry if present, append otherwise.
|
|
44
|
+
const existing = await new Promise((r) => {
|
|
45
|
+
execFile('crontab', ['-l'], (err, stdout) => r(err ? '' : stdout));
|
|
46
|
+
});
|
|
47
|
+
const lines = existing.split('\n').filter((l) => !l.includes(marker));
|
|
48
|
+
lines.push(cronLine);
|
|
49
|
+
await new Promise((r, j) => {
|
|
50
|
+
execFile('crontab', ['-'], { input: lines.join('\n') + '\n' }, (err) => err ? j(err) : r());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
cadence,
|
|
55
|
+
cronSpec,
|
|
56
|
+
panelPath: absolute,
|
|
57
|
+
marker,
|
|
58
|
+
next: cadence === 'weekly' ? 'next Monday 09:17 UTC' : `every ${CADENCE_HOURS[cadence]}h at :17`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function listWatches() {
|
|
63
|
+
const existing = await new Promise((r) => {
|
|
64
|
+
execFile('crontab', ['-l'], (err, stdout) => r(err ? '' : stdout));
|
|
65
|
+
});
|
|
66
|
+
return existing.split('\n')
|
|
67
|
+
.filter((l) => l.includes('# agentlinkops:citation-watch:'))
|
|
68
|
+
.map((l) => {
|
|
69
|
+
const marker = l.match(/agentlinkops:citation-watch:(\S+)/);
|
|
70
|
+
const cronPart = l.split(/\s+/).slice(0, 5).join(' ');
|
|
71
|
+
return { panel: marker?.[1] ?? 'unknown', cron: cronPart };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function removeWatch(panelName) {
|
|
76
|
+
const marker = `# agentlinkops:citation-watch:${panelName}`;
|
|
77
|
+
const existing = await new Promise((r) => {
|
|
78
|
+
execFile('crontab', ['-l'], (err, stdout) => r(err ? '' : stdout));
|
|
79
|
+
});
|
|
80
|
+
const lines = existing.split('\n').filter((l) => !l.includes(marker));
|
|
81
|
+
await new Promise((r, j) => {
|
|
82
|
+
execFile('crontab', ['-'], { input: lines.join('\n') + '\n' }, (err) => err ? j(err) : r());
|
|
83
|
+
});
|
|
84
|
+
return { removed: panelName };
|
|
85
|
+
}
|
|
File without changes
|
|
File without changes
|
package/src/context/ga4.js
CHANGED
|
File without changes
|
package/src/context/gsc.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/context/util.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/corpus/acquire.js
CHANGED
|
File without changes
|
package/src/corpus/seeds.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/discovery/import.js
CHANGED
|
File without changes
|
package/src/tool-views.js
CHANGED
|
File without changes
|
package/src/verifier/fetch.js
CHANGED
|
File without changes
|
package/src/verifier/index.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/verifier/parser.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/verifier/robots.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/verifier/url.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|