agentlinkops 0.6.11 → 0.6.13
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/CHANGELOG.md +12 -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/admission.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 +4435 -175
- package/cli/check.js +0 -0
- package/cli/chronology.js +39 -0
- package/cli/citation.js +0 -0
- package/cli/client.js +1 -1
- package/cli/cloud-error.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/disavow.js +0 -0
- package/cli/doctor.js +0 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +30 -19
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/index-observations.js +0 -0
- package/cli/index-submissions.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/lifecycle.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +34 -8
- package/cli/mcp-client.js +2 -2
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/native-lock.js +0 -0
- package/cli/oauth-credentials.js +0 -0
- package/cli/oauth-flow.js +0 -0
- package/cli/placement-projection.js +106 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +51 -19
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +62 -29
- package/cli/status.js +14 -18
- package/cli/sync-lock.js +0 -0
- package/cli/sync.js +22 -3
- package/package.json +1 -1
- package/references/citation-panels.md +0 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +0 -0
- package/references/local-crm-draft-handoff.md +0 -0
- package/references/portable-ledger-handoff.md +0 -0
- package/references/recipes/catalog.json +0 -0
- package/references/recipes/command-notes.md +0 -0
- package/references/recipes/placement-reconciliation.md +0 -0
- package/references/recipes/qualified-campaign-handoff.md +0 -0
- package/references/recipes/site-context-brief.md +0 -0
- package/references/recipes/sourced-linkable-asset.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/outreach-crm-draft.py +0 -0
- package/scripts/portable-handoff.py +0 -0
- package/scripts/report.py +0 -0
- package/shared/admission-contract.js +0 -0
- package/shared/admission-report-contract.js +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +0 -0
- package/shared/disavow-contract.js +0 -0
- package/shared/exec-grammar.js +0 -0
- package/shared/index-observation-contract.js +2 -2
- package/shared/lifecycle-contract.js +0 -0
- package/shared/lifecycle-money.js +0 -0
- package/shared/monitoring-chronology-export.js +57 -0
- package/shared/monitoring-read-contract.js +41 -0
- package/shared/observation-event.js +0 -0
- package/shared/opportunity-record.js +0 -0
- package/shared/response-contract.js +3 -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 +4 -3
- package/skills/agentlinkops-crm/SKILL.md +0 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +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 +0 -0
- package/src/citations/alert.js +0 -0
- package/src/citations/browser/chatgpt-citations.js +0 -0
- package/src/citations/browser/engine.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/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/extraction/sourceUtils.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/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/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 +0 -0
- package/src/citations/browser/proxy-admission.js +0 -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 +0 -0
- package/src/citations/egress-receipts.js +0 -0
- package/src/citations/evidence-integrity.js +0 -0
- package/src/citations/inventory.js +0 -0
- package/src/citations/login.js +0 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +0 -0
- package/src/citations/report.js +0 -0
- package/src/citations/runner.js +0 -0
- package/src/citations/stats.js +0 -0
- package/src/citations/sync.js +0 -0
- package/src/citations/watch.js +0 -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/evidence-reference.js +0 -0
- package/src/index-observation.js +0 -0
- package/src/locator.js +0 -0
- package/src/response-shaping.js +0 -0
- package/src/tool-views.js +5 -4
- 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
package/cli/report.js
CHANGED
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
// evidence reference that resolves to the observation itself. One dataset feeds both exports:
|
|
19
19
|
// this HTML view for a human client, and `report --json` for an agent, so the two can never
|
|
20
20
|
// disagree about what was observed.
|
|
21
|
+
// v3 freezes activity separately from retained receipts. Older v2 datasets still render with
|
|
22
|
+
// their original columns and wording; activity never changes an old observation's date/hash.
|
|
21
23
|
import {indexRowsForUrl} from './index-observations.js';
|
|
22
24
|
import {reconcileIndexObservations} from '../src/index-observation.js';
|
|
23
25
|
import { createHash } from 'node:crypto';
|
|
24
|
-
import {
|
|
26
|
+
import { projectPlacements } from './placement-projection.js';
|
|
25
27
|
import { mirrorEvidenceReference } from '../src/evidence-reference.js';
|
|
26
28
|
|
|
27
|
-
export const REPORT_VERSION =
|
|
29
|
+
export const REPORT_VERSION = 3;
|
|
28
30
|
|
|
29
31
|
/** HTML-escape. Every value below is publisher or customer content and none of it is trusted. */
|
|
30
32
|
export const escape = value => String(value ?? '')
|
|
@@ -35,6 +37,10 @@ const STATE_LABEL = Object.freeze({
|
|
|
35
37
|
present: 'Present', absent: 'Not found', unknown: 'Could not check',
|
|
36
38
|
source_unavailable: 'Page unavailable', unchecked: 'Never checked',
|
|
37
39
|
});
|
|
40
|
+
const CURRENT_STATE_LABEL = Object.freeze({
|
|
41
|
+
...STATE_LABEL,
|
|
42
|
+
suspected_missing: 'Suspected missing', confirmed_missing: 'Confirmed missing',
|
|
43
|
+
});
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* The frozen dataset a report renders.
|
|
@@ -42,12 +48,10 @@ const STATE_LABEL = Object.freeze({
|
|
|
42
48
|
* Selection happens here and nowhere else, so a caller can hold this object, assert it, and know
|
|
43
49
|
* the rendered document is a pure function of it.
|
|
44
50
|
*/
|
|
45
|
-
export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false, indexObservations = [] } = {}) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.map(entry => {
|
|
50
|
-
const row = latest.get(entry.id) ?? null;
|
|
51
|
+
export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false, indexObservations = [], activityState = null } = {}) {
|
|
52
|
+
const rows = projectPlacements(entries.filter(entry => includeRetired || entry.intent !== 'retired'), observations, { state: activityState })
|
|
53
|
+
.map(projection => {
|
|
54
|
+
const { entry, row } = projection;
|
|
51
55
|
const indexRows=indexRowsForUrl(indexObservations,entry.source);
|
|
52
56
|
const origin = row?.source === 'cloud' ? 'cloud' : (row?.source ?? null);
|
|
53
57
|
return {
|
|
@@ -57,6 +61,16 @@ export function freezeDataset(entries, observations, { asOf, title = 'Backlink r
|
|
|
57
61
|
reason: row?.reason ?? null,
|
|
58
62
|
occurrences: row?.occurrences ?? 0,
|
|
59
63
|
checked_at: row?.checked_at ?? null,
|
|
64
|
+
current_state: projection.current_state,
|
|
65
|
+
uncertain: projection.uncertain,
|
|
66
|
+
activity: {
|
|
67
|
+
available: projection.activity_available,
|
|
68
|
+
identity_status: projection.identity_status,
|
|
69
|
+
latest_attempt: projection.latest_attempt,
|
|
70
|
+
last_successful_observation: projection.last_successful_observation,
|
|
71
|
+
last_link_verification: projection.last_link_verification,
|
|
72
|
+
first_present: projection.first_present,
|
|
73
|
+
},
|
|
60
74
|
checked_by: row?.source ?? null,
|
|
61
75
|
anchor: row?.result?.occurrences?.[0]?.anchor ?? null,
|
|
62
76
|
rel: row?.result?.occurrences?.[0]?.rel ?? [],
|
|
@@ -105,10 +119,16 @@ export function freezeDataset(entries, observations, { asOf, title = 'Backlink r
|
|
|
105
119
|
// Stated rather than implied. A report over a ledger nobody has checked is not a report about
|
|
106
120
|
// links, and the reader should see that before the table.
|
|
107
121
|
coverage: {
|
|
108
|
-
checked: rows.filter(row => row.
|
|
109
|
-
never_checked: rows.filter(row => !row.
|
|
122
|
+
checked: rows.filter(row => row.activity.latest_attempt).length,
|
|
123
|
+
never_checked: rows.filter(row => !row.activity.latest_attempt).length,
|
|
124
|
+
retained_receipt_entries: rows.filter(row => row.checked_at).length,
|
|
125
|
+
missing_receipt_entries: rows.filter(row => !row.checked_at).length,
|
|
110
126
|
observed_from: dates[0] ?? null,
|
|
111
127
|
observed_to: dates.at(-1) ?? null,
|
|
128
|
+
activity_entries: rows.filter(row => row.activity.available).length,
|
|
129
|
+
identity_unmatched_entries: rows.filter(row => row.activity.identity_status === 'not_matched').length,
|
|
130
|
+
denominator: includeRetired ? 'all_ledger_entries' : 'non_retired_ledger_entries',
|
|
131
|
+
known_present_after_unknown: rows.filter(row => row.current_state === 'present' && row.uncertain).length,
|
|
112
132
|
},
|
|
113
133
|
};
|
|
114
134
|
}
|
|
@@ -149,6 +169,18 @@ export function renderReport(dataset, { brand = null } = {}) {
|
|
|
149
169
|
const digest = createHash('sha256').update(JSON.stringify(dataset)).digest('hex');
|
|
150
170
|
const card = (label, value) => `<div class="card"><b>${escape(value)}</b><span>${escape(label)}</span></div>`;
|
|
151
171
|
const coverage = dataset.coverage;
|
|
172
|
+
const hasActivity = dataset.v >= 3;
|
|
173
|
+
const activityCell = row => {
|
|
174
|
+
const activity = row.activity ?? {};
|
|
175
|
+
const describe = (label, attempt) => `<div>${escape(label)}: ${attempt ? `${escape(attempt.state)} · ${escape(attempt.checked_at)}` : 'not retained'}</div>`
|
|
176
|
+
+ (attempt ? `<div class="note">${escape(attempt.reason ?? 'reason not recorded')} · ${escape(attempt.method ?? 'method not recorded')}</div>`
|
|
177
|
+
+ `<div class="ref">${escape(attempt.evidence_reference ?? 'no retained receipt reference')}</div>` : '');
|
|
178
|
+
return `<div>${escape(CURRENT_STATE_LABEL[row.current_state] ?? row.current_state ?? 'Unknown')}${row.uncertain ? ' · latest attempt inconclusive' : ''}</div>`
|
|
179
|
+
+ describe('Latest attempt', activity.latest_attempt)
|
|
180
|
+
+ describe('Last conclusive check', activity.last_successful_observation)
|
|
181
|
+
+ describe('Last link verification', activity.last_link_verification)
|
|
182
|
+
+ `<div class="note">Activity ${activity.available ? 'recorded' : 'not supplied or not retained'}; identity ${escape(activity.identity_status ?? 'not retained')}</div>`;
|
|
183
|
+
};
|
|
152
184
|
// The receipt column. "hash not recorded" is stated rather than papered over: a local check
|
|
153
185
|
// that never completed a fetch has no hash, and a cloud-synced row may carry its snapshot key
|
|
154
186
|
// without a hash in an older event. Neither is a claim about the link.
|
|
@@ -164,10 +196,10 @@ export function renderReport(dataset, { brand = null } = {}) {
|
|
|
164
196
|
const rows = dataset.rows.map(row => `<tr>
|
|
165
197
|
<td class="url"><a href="${escape(row.source)}">${escape(row.source)}</a></td>
|
|
166
198
|
<td class="url">${escape(row.target)}</td>
|
|
167
|
-
<td><span class="state ${escape(row.state)}">${escape(STATE_LABEL[row.state] ?? row.state)}</span></td>
|
|
199
|
+
<td><span class="state ${escape(row.state)}">${escape(hasActivity && !row.evidence && row.activity?.latest_attempt ? 'No retained observation' : (hasActivity ? CURRENT_STATE_LABEL : STATE_LABEL)[row.state] ?? row.state)}</span></td>
|
|
168
200
|
<td>${row.anchor ? escape(row.anchor) : '<span class="note">—</span>'}${(row.rel ?? []).length ? `<div class="note">rel: ${escape(row.rel.join(' '))}</div>` : ''}</td>
|
|
169
201
|
<td>${row.checked_at ? escape(row.checked_at.slice(0, 10)) : '<span class="note">never</span>'}</td>
|
|
170
|
-
<td>${evidenceCell(row)}</td
|
|
202
|
+
<td>${evidenceCell(row)}</td>${hasActivity ? `\n <td>${activityCell(row)}</td>` : ''}
|
|
171
203
|
${hasIndex?`<td>${(row.index_observations??[]).map(o=>`${escape(o.tier)} · ${escape(o.reason)}<div>${escape(o.checked_at)} · ${escape(o.backend)}</div><div class="ref">${escape(o.locator)}</div>`).join('<hr>')||'Not checked'}</td>`:''}
|
|
172
204
|
${dataset.include_notes ? `<td class="note">${escape(row.ref ?? '')}${row.note ? `<div>${escape(row.note)}</div>` : ''}</td>` : ''}
|
|
173
205
|
</tr>`).join('\n');
|
|
@@ -188,21 +220,21 @@ export function renderReport(dataset, { brand = null } = {}) {
|
|
|
188
220
|
<p class="sub">${brand ? `${escape(brand)} · ` : ''}Prepared ${escape(dataset.as_of)}</p>
|
|
189
221
|
<div class="cards">
|
|
190
222
|
${card('links in report', dataset.totals.entries)}
|
|
191
|
-
${card('present', dataset.totals.present ?? 0)}
|
|
192
|
-
${card('not found', dataset.totals.absent ?? 0)}
|
|
193
|
-
${card('could not check', (dataset.totals.unknown ?? 0) + (dataset.totals.source_unavailable ?? 0))}
|
|
194
|
-
${card('never checked', dataset.totals.unchecked ?? 0)}
|
|
223
|
+
${card(hasActivity ? 'retained present' : 'present', dataset.totals.present ?? 0)}
|
|
224
|
+
${card(hasActivity ? 'retained not found' : 'not found', dataset.totals.absent ?? 0)}
|
|
225
|
+
${card(hasActivity ? 'retained could not check' : 'could not check', (dataset.totals.unknown ?? 0) + (dataset.totals.source_unavailable ?? 0))}
|
|
226
|
+
${card('never checked', hasActivity ? coverage.never_checked : dataset.totals.unchecked ?? 0)}
|
|
195
227
|
</div>
|
|
196
228
|
<p class="caveat"><strong>What these dates mean.</strong> Every row shows the date that link was
|
|
197
229
|
last observed, between ${escape(coverage.observed_from ?? 'n/a')} and ${escape(coverage.observed_to ?? 'n/a')}.
|
|
198
230
|
The date at the top is when this document was prepared and does not make any observation newer
|
|
199
|
-
than it is. ${coverage.never_checked ? `${coverage.never_checked} of ${dataset.totals.entries} links have never been checked and are listed as such.` : ''}</p>
|
|
231
|
+
than it is. ${coverage.never_checked ? `${coverage.never_checked} of ${dataset.totals.entries} links have never been checked and are listed as such.` : ''}</p>${hasActivity ? '\n<p class="caveat"><strong>Activity and verification.</strong> The result and receipt date describe the retained observation. Activity lists later attempts and complete checks separately; an unchanged recheck can be newer than that receipt. An inconclusive attempt preserves the earlier verified state, and does not prove loss. A complete unavailable source is a conclusive source check, not a link verification. Missing activity is not inferred from a report preparation date.</p>' : ''}
|
|
200
232
|
<p class="caveat"><strong>What a result means.</strong> “Present” means the link was in
|
|
201
233
|
the observed HTML. “Could not check” is not the same as
|
|
202
234
|
“not found”: it means the page could not be read, and no conclusion was drawn.
|
|
203
235
|
Each receipt identifies the check method when it was recorded. A snapshot alone does not prove the link was visible on screen.</p>
|
|
204
236
|
<table><thead><tr>
|
|
205
|
-
<th>Source page</th><th>Target</th><th>Result</th><th>Anchor</th><th>Observed</th><th>Evidence</th>${hasIndex?'<th>Index evidence</th>':''}${dataset.include_notes ? '<th>Notes</th>' : ''}
|
|
237
|
+
<th>Source page</th><th>Target</th><th>Result</th><th>Anchor</th><th>Observed</th><th>Evidence</th>${hasActivity ? '<th>Activity and verified state</th>' : ''}${hasIndex?'<th>Index evidence</th>':''}${dataset.include_notes ? '<th>Notes</th>' : ''}
|
|
206
238
|
</tr></thead><tbody>
|
|
207
239
|
${rows}
|
|
208
240
|
</tbody></table>
|
|
@@ -219,7 +251,7 @@ ${receiptRows}
|
|
|
219
251
|
</tbody></table>` : ''}
|
|
220
252
|
<footer>
|
|
221
253
|
AgentLinkOps report v${dataset.v}. Dataset digest <code>${escape(digest)}</code> — the same
|
|
222
|
-
ledger and observations always produce this digest. The same dataset exports as JSON
|
|
254
|
+
ledger and observations${hasActivity ? ' and frozen activity' : ''} always produce this digest. The same dataset exports as JSON
|
|
223
255
|
(<code>agentlinkops report --json</code>) and carries the same digest.
|
|
224
256
|
${dataset.include_notes ? 'Private notes are included in this document.' : 'Private notes and references are excluded.'}
|
|
225
257
|
</footer>
|
package/cli/setup-plan.js
CHANGED
|
File without changes
|
package/cli/skill.js
CHANGED
|
File without changes
|
package/cli/state.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// was this last confirmed" — is exactly what a small, rewritten map answers better.
|
|
11
11
|
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
|
+
import { placementIdentity, attemptSummary, isConclusive, isLinkVerification, validTime } from './placement-projection.js';
|
|
13
14
|
|
|
14
|
-
export const STATE_VERSION =
|
|
15
|
+
export const STATE_VERSION = 3;
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Brings a state file forward without losing anything it already held.
|
|
@@ -58,49 +59,81 @@ export async function writeState(path, state) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
/** When each entry was last checked, whatever the answer was. */
|
|
61
|
-
export function lastCheckedMap(state) {
|
|
62
|
-
|
|
62
|
+
export function lastCheckedMap(state, entries = null) {
|
|
63
|
+
const identities = entries ? new Map(entries.map(entry => [entry.id, placementIdentity(entry)])) : null;
|
|
64
|
+
return new Map(Object.entries(state.entries ?? {}).filter(([id, record]) => !identities
|
|
65
|
+
|| identities.get(id) && record.last_placement === identities.get(id)).map(([id, record]) => [id, record.last_checked]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const methodOf = row => row.result?.evidence?.method ?? null;
|
|
69
|
+
const eligible = (row, previous) => validTime(row.checked_at)
|
|
70
|
+
&& (!validTime(previous?.last_checked) || Date.parse(row.checked_at) > Date.parse(previous.last_checked));
|
|
71
|
+
const repeats = (row, previous) => previous
|
|
72
|
+
&& previous.last_placement === placementIdentity(row)
|
|
73
|
+
&& previous.last_state === row.state
|
|
74
|
+
&& (previous.last_reason ?? null) === (row.reason ?? null)
|
|
75
|
+
&& (previous.last_signature ?? null) === (row.result?.linkSignature ?? null)
|
|
76
|
+
&& previous.last_complete === (row.complete === true)
|
|
77
|
+
&& (previous.last_method ?? null) === methodOf(row)
|
|
78
|
+
&& (previous.last_checker_version ?? null) === (row.checker_version ?? row.result?.evidence?.checkerVersion ?? null);
|
|
79
|
+
|
|
80
|
+
function applyRow(previous, row) {
|
|
81
|
+
const identity = placementIdentity(row);
|
|
82
|
+
const samePlacement = previous?.last_placement === identity;
|
|
83
|
+
const attempt = attemptSummary(row, { retained: !repeats(row, previous) });
|
|
84
|
+
const knownPresent = isConclusive(attempt) && attempt.state === 'present';
|
|
85
|
+
const priorFirst = samePlacement && previous?.first_present_observation?.placement === identity
|
|
86
|
+
&& isConclusive(previous.first_present_observation) && previous.first_present_observation.state === 'present'
|
|
87
|
+
? previous.first_present_observation : null;
|
|
88
|
+
const firstPresent = priorFirst ?? (knownPresent ? attempt : null);
|
|
89
|
+
return {
|
|
90
|
+
...previous,
|
|
91
|
+
last_checked: row.checked_at,
|
|
92
|
+
last_complete: row.complete === true,
|
|
93
|
+
last_placement: identity,
|
|
94
|
+
last_state: row.state,
|
|
95
|
+
last_reason: row.reason ?? null,
|
|
96
|
+
last_signature: row.result?.linkSignature ?? null,
|
|
97
|
+
last_method: methodOf(row),
|
|
98
|
+
last_checker_version: row.checker_version ?? row.result?.evidence?.checkerVersion ?? null,
|
|
99
|
+
checks: (previous?.checks ?? 0) + 1,
|
|
100
|
+
last_attempt: attempt,
|
|
101
|
+
last_successful_observation: isConclusive(attempt) ? attempt : samePlacement ? previous?.last_successful_observation ?? null : null,
|
|
102
|
+
last_link_verification: isLinkVerification(attempt) ? attempt : samePlacement ? previous?.last_link_verification ?? null : null,
|
|
103
|
+
first_present: firstPresent?.checked_at ?? null,
|
|
104
|
+
first_present_placement: firstPresent ? identity : null,
|
|
105
|
+
first_present_observation: firstPresent,
|
|
106
|
+
...(samePlacement && previous?.first_present && !previous.first_present_observation
|
|
107
|
+
? { legacy_first_present: previous.first_present } : {}),
|
|
108
|
+
};
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
/**
|
|
66
112
|
* Splits a run's rows into the ones worth recording and the ones that repeat.
|
|
67
113
|
*
|
|
68
|
-
* A row is worth recording when it is the first for its
|
|
69
|
-
*
|
|
70
|
-
*
|
|
114
|
+
* A row is worth recording when it is the first for its placement, or when the state, reason,
|
|
115
|
+
* link signature, completeness or method moved. A link that is still present but whose anchor
|
|
116
|
+
* or rel changed is a real event, and comparing only the state would miss it.
|
|
71
117
|
*/
|
|
72
118
|
export function selectChanged(rows, state) {
|
|
73
|
-
const changed = [], repeated = [];
|
|
119
|
+
const changed = [], repeated = [], ignored = [];
|
|
120
|
+
const entries = { ...(state.entries ?? {}) };
|
|
74
121
|
for (const row of rows) {
|
|
75
|
-
const previous =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
&& (previous.last_reason ?? null) === (row.reason ?? null)
|
|
80
|
-
&& (previous.last_signature ?? null) === signature;
|
|
81
|
-
(same ? repeated : changed).push(row);
|
|
122
|
+
const previous = entries[row.id];
|
|
123
|
+
if (!eligible(row, previous)) { ignored.push(row); continue; }
|
|
124
|
+
(repeats(row, previous) ? repeated : changed).push(row);
|
|
125
|
+
entries[row.id] = applyRow(previous, row);
|
|
82
126
|
}
|
|
83
|
-
return { changed, repeated };
|
|
127
|
+
return { changed, repeated, ignored };
|
|
84
128
|
}
|
|
85
129
|
|
|
86
130
|
/** Applies a run to the activity record. Every check counts, recorded or not. */
|
|
87
131
|
export function applyRun(state, rows) {
|
|
88
132
|
const entries = { ...(state.entries ?? {}) };
|
|
89
133
|
for (const row of rows) {
|
|
90
|
-
const previous = entries[row.id]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
last_checked: row.checked_at,
|
|
94
|
-
last_complete: row.complete === true,
|
|
95
|
-
last_placement: row.result?.sourceUrl && row.result?.targetUrl ? JSON.stringify([row.result.sourceUrl, row.result.targetUrl, row.result.targetScope ?? 'exact']) : null,
|
|
96
|
-
last_state: row.state,
|
|
97
|
-
last_reason: row.reason ?? null,
|
|
98
|
-
last_signature: row.result?.linkSignature ?? null,
|
|
99
|
-
checks: (previous.checks ?? 0) + 1,
|
|
100
|
-
// The date a placement was first seen is worth keeping where nothing can compact it away.
|
|
101
|
-
first_present: previous.first_present ?? (row.state === 'present' ? row.checked_at : null),
|
|
102
|
-
first_present_placement: previous.first_present ? previous.first_present_placement ?? null : row.state === 'present' && row.result?.sourceUrl && row.result?.targetUrl ? JSON.stringify([row.result.sourceUrl, row.result.targetUrl, row.result.targetScope ?? 'exact']) : null,
|
|
103
|
-
};
|
|
134
|
+
const previous = entries[row.id];
|
|
135
|
+
if (!eligible(row, previous)) continue;
|
|
136
|
+
entries[row.id] = applyRow(previous, row);
|
|
104
137
|
}
|
|
105
138
|
return { ...state, v: STATE_VERSION, entries };
|
|
106
139
|
}
|
package/cli/status.js
CHANGED
|
@@ -4,32 +4,28 @@
|
|
|
4
4
|
// expectation, closes the laptop, and three weeks later an agent opens the repository and reads
|
|
5
5
|
// this. So it answers in the order a person cares about, not the order the data is stored in:
|
|
6
6
|
// what appeared, what was lost, what we cannot say, and what needs a decision.
|
|
7
|
-
import {
|
|
7
|
+
import { projectPlacements } from './placement-projection.js';
|
|
8
8
|
|
|
9
9
|
/** A ledger entry whose observations disagree with its declared intent. */
|
|
10
|
-
export function disagreements(entries, observations) {
|
|
11
|
-
const latest = latestByEntry(observations);
|
|
10
|
+
export function disagreements(entries, observations, { state = null } = {}) {
|
|
12
11
|
const out = [];
|
|
13
|
-
for (const
|
|
14
|
-
const row =
|
|
15
|
-
|
|
12
|
+
for (const projection of projectPlacements(entries, observations, { state })) {
|
|
13
|
+
const { entry, row, latest_attempt: attempt } = projection;
|
|
14
|
+
const activity = { current_state: projection.current_state, uncertain: projection.uncertain,
|
|
15
|
+
latest_attempt: attempt, last_successful_observation: projection.last_successful_observation,
|
|
16
|
+
last_link_verification: projection.last_link_verification,
|
|
17
|
+
evidence_observed_at: projection.evidence_observed_at, identity_status: projection.identity_status };
|
|
18
|
+
if (!attempt) { out.push({ entry, kind: 'never_checked', activity }); continue; }
|
|
16
19
|
if (entry.intent === 'expected') {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|| (row.state === 'unknown' && hostedState === 'suspected_missing');
|
|
22
|
-
const missing = ['absent', 'suspected_missing', 'confirmed_missing'].includes(row.state);
|
|
23
|
-
// The change mirror omits repeated checks, so raw absence cannot prove confirmation.
|
|
24
|
-
if ((confirmed || suspected) && row.complete === true) {
|
|
25
|
-
out.push({ entry, row, kind: confirmed ? 'lost' : 'suspected_missing' });
|
|
26
|
-
} else if (missing || ['unknown', 'source_unavailable'].includes(row.state)) {
|
|
27
|
-
out.push({ entry, row, kind: 'cannot_say' });
|
|
20
|
+
if (projection.uncertain || attempt.state === 'source_unavailable') {
|
|
21
|
+
out.push({ entry, row, kind: 'cannot_say', activity });
|
|
22
|
+
} else if (['suspected_missing', 'confirmed_missing'].includes(projection.current_state)) {
|
|
23
|
+
out.push({ entry, row, kind: projection.current_state === 'confirmed_missing' ? 'lost' : 'suspected_missing', activity });
|
|
28
24
|
}
|
|
29
25
|
}
|
|
30
26
|
// Nothing is promoted automatically: whether a link that appeared is one we now EXPECT is
|
|
31
27
|
// a judgement about a relationship, not about HTML. So this proposes the edit and stops.
|
|
32
|
-
if (entry.intent === 'wanted' &&
|
|
28
|
+
if (entry.intent === 'wanted' && !projection.uncertain && projection.current_state === 'present') out.push({ entry, row, kind: 'appeared', activity, suggest: { intent: 'expected' } });
|
|
33
29
|
}
|
|
34
30
|
return out;
|
|
35
31
|
}
|
package/cli/sync-lock.js
CHANGED
|
File without changes
|
package/cli/sync.js
CHANGED
|
@@ -12,6 +12,24 @@ import { CloudError } from './client.js';
|
|
|
12
12
|
|
|
13
13
|
const PUSH_BATCH = 100;
|
|
14
14
|
|
|
15
|
+
function completeImportRows(result, expected) {
|
|
16
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
18
|
+
const refuse = () => { throw new CloudError('INVALID_IMPORT_RESPONSE', 502, null,
|
|
19
|
+
'The cloud returned an incomplete or invalid watch import result. Some watches may already have been stored. Inspect accessible watches and their local references before retrying sync; this request was not automatically replayed.'); };
|
|
20
|
+
if (!record(result) || !Array.isArray(result.rows) || result.rows.length !== expected) refuse();
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
for (const row of result.rows) {
|
|
23
|
+
if (!record(row) || !Number.isInteger(row.index) || row.index < 0 || row.index >= expected || seen.has(row.index)) refuse();
|
|
24
|
+
seen.add(row.index);
|
|
25
|
+
const watch = Object.hasOwn(row, 'watch'), error = Object.hasOwn(row, 'error');
|
|
26
|
+
if (watch === error) refuse();
|
|
27
|
+
if (watch && (!record(row.watch) || !text(row.watch.id) || typeof row.watch.created !== 'boolean')) refuse();
|
|
28
|
+
if (error && (!record(row.error) || !text(row.error.code) || !text(row.error.message))) refuse();
|
|
29
|
+
}
|
|
30
|
+
return result.rows;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export function syncPlan(entries, state, { includeWanted = false, ledgerIds = null } = {}) {
|
|
16
34
|
if (ledgerIds !== null && (!Array.isArray(ledgerIds) || ledgerIds.some(id => typeof id !== 'string') || ledgerIds.some(id => !entries.some(entry => entry.id === id))))
|
|
17
35
|
throw new CloudError('INVALID_SYNC_SELECTION', 0);
|
|
@@ -38,12 +56,13 @@ export async function pushExpectations(client, entries, state, { projectId, onPr
|
|
|
38
56
|
for (let index = 0; index < watched.length; index += PUSH_BATCH) {
|
|
39
57
|
const batch = watched.slice(index, index + PUSH_BATCH);
|
|
40
58
|
const result = await client.importWatches(projectId, batch.map(toWatchInput));
|
|
41
|
-
|
|
59
|
+
// Validate the complete acknowledgement before adopting any of this batch.
|
|
60
|
+
// Earlier POSTs may have committed; a malformed response never authorizes a replay.
|
|
61
|
+
for (const row of completeImportRows(result, batch.length)) {
|
|
42
62
|
const entry = batch[row.index];
|
|
43
|
-
if (!entry) continue;
|
|
44
63
|
if (row.error) { failed.push({ id: entry.id, code: row.error.code, message: row.error.message }); continue; }
|
|
45
64
|
mapping[entry.id] = row.watch.id;
|
|
46
|
-
if (row.watch.created
|
|
65
|
+
if (row.watch.created) created.push(entry.id);
|
|
47
66
|
}
|
|
48
67
|
onProgress?.(Math.min(index + PUSH_BATCH, watched.length), watched.length);
|
|
49
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlinkops",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "Backlink checks with dated evidence and portable results, optional repository ledgers, supplier-export imports and sync to AgentLinkOps.",
|
|
5
5
|
"author": "AgentLinkOps (https://agentlinkops.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
File without changes
|
package/references/cli.md
CHANGED
|
File without changes
|
package/references/disavow.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/references/reports.md
CHANGED
|
File without changes
|
package/scripts/agentlinkops.py
CHANGED
|
File without changes
|
package/scripts/disavow.py
CHANGED
|
File without changes
|
package/scripts/linktrail.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/scripts/report.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/shared/agent-clients.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/shared/exec-grammar.js
CHANGED
|
File without changes
|
|
@@ -4,7 +4,7 @@ export const indexConfigureInput=z.object({projectId:id,enabled:z.boolean(),dail
|
|
|
4
4
|
export const indexInspectInput=z.object({connectionId:id,watchId:id,idempotencyKey:z.string().regex(/^[\x21-\x7e]{1,128}$/)}).strict();
|
|
5
5
|
export const indexHistoryInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
6
6
|
export const indexReceiptInput=z.object({projectId:id,watchId:id,receiptId:digest}).strict();
|
|
7
|
-
export const indexEventsInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional()}).strict();
|
|
7
|
+
export const indexEventsInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
8
8
|
export const indexSettingsInput=z.object({projectId:id}).strict();
|
|
9
9
|
export const indexSettingsResponse=z.object({projectId:id,enabled:z.boolean(),dailyLimit:z.number().int().positive(),retentionDays:z.number().int().positive(),cadenceSeconds:z.number().int().positive(),schedulingAvailable:z.boolean()}).passthrough();
|
|
10
10
|
export const indexObservation=z.looseObject({id:digest,observationId:id,checkedAt:time,recordedAt:time,expiresAt:time,evidenceSha256:digest,eventId:digest.nullable(),ignored:z.string().nullable(),version:z.literal(1),url:z.url(),lane:z.enum(['gsc','licensed_site']),backend:z.string(),source_key:z.string(),tier:z.enum(['indexed','not_indexed','likely_indexed','not_found_in_site_query','unknown']),confidence:z.enum(['google_index_snapshot','search_result_sample','unknown']),reason:z.string(),live_page_test:z.literal(false),submission_requested:z.literal(false)});
|
|
@@ -13,4 +13,4 @@ export const indexInspectResponse=indexReceiptResponse.extend({replayed:z.boolea
|
|
|
13
13
|
export const indexAttemptSummary=z.object({observedAt:time,latestAttempt:z.object({id:digest,admittedAt:time,status:z.enum(['in_progress','unsettled','completed']),receiptId:digest.nullable(),leaseExpiresAt:time.nullable()}).nullable(),unsettledCount:z.number().int().nonnegative()});
|
|
14
14
|
export const indexWebsiteQuota=z.object({windowStart:time,observedAt:time,admitted:z.number().int().nonnegative(),limit:z.number().int().positive(),remaining:z.number().int().nonnegative()});
|
|
15
15
|
export const indexHistoryResponse=z.object({observations:z.array(indexObservation),nextCursor:digest.nullable(),attemptSummary:indexAttemptSummary.optional(),websiteQuota:indexWebsiteQuota.optional()}).passthrough();
|
|
16
|
-
export const indexEventsResponse=z.object({events:z.array(z.looseObject({id:digest,receiptId:digest,type:z.enum(['index.deindexed','index.reindexed']),createdAt:time,url:z.url(),source_key:z.string(),before:z.enum(['indexed','not_indexed']),after:z.enum(['indexed','not_indexed']),observation_ids:z.array(id)}))}).passthrough();
|
|
16
|
+
export const indexEventsResponse=z.object({events:z.array(z.looseObject({id:digest,receiptId:digest,type:z.enum(['index.deindexed','index.reindexed']),createdAt:time,url:z.url(),source_key:z.string(),before:z.enum(['indexed','not_indexed']),after:z.enum(['indexed','not_indexed']),observation_ids:z.array(id)})),nextCursor:digest.nullable().optional(),hasMore:z.boolean().optional()}).passthrough();
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Offline frozen chronology export. No hosted store, authentication, network or
|
|
2
|
+
// collection dependency belongs in the customer's CLI export closure.
|
|
3
|
+
const fail=(status,code,message)=>{throw Object.assign(new Error(message),{name:'ChronologyExportError',status,code});};
|
|
4
|
+
const iso=value=>typeof value==='string'&&Number.isFinite(Date.parse(value))?new Date(value).toISOString():null;
|
|
5
|
+
const validIdentity=value=>{
|
|
6
|
+
if(!value||typeof value!=='object'||Array.isArray(value))return false;
|
|
7
|
+
const id=v=>typeof v==='string'&&v.length>0&&v.length<=200&&!/[\s\x00-\x1f\x7f]/u.test(v);
|
|
8
|
+
const url=v=>{if(typeof v!=='string'||!v||v.length>4096)return false;try{const u=new URL(v);return ['http:','https:'].includes(u.protocol)&&!u.username&&!u.password;}catch{return false;}};
|
|
9
|
+
return ['workspaceId','projectId','watchId'].every(k=>id(value[k]))&&url(value.sourceUrl)&&url(value.targetUrl)
|
|
10
|
+
&&['exact','domain','subdomain','path'].includes(value.targetScope)&&(value.targetId===null||id(value.targetId))
|
|
11
|
+
&&(value.localReference===null||typeof value.localReference==='string'&&value.localReference.length<=2048)
|
|
12
|
+
&&value.matchBasis==='configured_target_url';
|
|
13
|
+
};
|
|
14
|
+
const compare=(a,b)=>a.sortAt>b.sortAt?-1:a.sortAt<b.sortAt?1:a.id>b.id?-1:a.id<b.id?1:0;
|
|
15
|
+
const sha256=async value=>Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(value))),b=>b.toString(16).padStart(2,'0')).join('');
|
|
16
|
+
const baseItem=(stream,row,identity,observedAt,recordedAt,kind)=>({id:`${stream}:${row.id}`,rawId:row.id,stream,kind,identity,observedAt:iso(observedAt),recordedAt:iso(recordedAt),sortAt:iso(observedAt)??iso(recordedAt),state:null,reason:null,sourceKey:null,method:null,checkerVersion:null,evidence:{sha256:null,reference:null,expiresAt:null,availability:'not_checked'},data:{}});
|
|
17
|
+
|
|
18
|
+
// Export only a supplied frozen dataset. This function never reads current DB
|
|
19
|
+
// state or fills a missing origin with today's projection.
|
|
20
|
+
export async function freezeChronology(pages,{localItems=[]}={}){
|
|
21
|
+
if(!Array.isArray(pages)||!pages.length||pages.length>10000||!Array.isArray(localItems)||localItems.length>10000)fail(400,'INVALID_CHRONOLOGY_EXPORT','Provide bounded chronology pages.');
|
|
22
|
+
const first=pages[0];if(!validIdentity(first?.identity)||iso(first?.asOf)!==first.asOf||!/^[a-f0-9]{64}$/.test(first?.snapshotId??''))fail(400,'INVALID_CHRONOLOGY_EXPORT','A frozen page requires an exact placement identity, watermark and snapshot digest.');
|
|
23
|
+
const byId=new Map();let continuous=first.pageCursor===null;
|
|
24
|
+
for(let i=0;i<pages.length;i++){
|
|
25
|
+
const page=pages[i];if(!page||page.schemaVersion!==1||page.snapshotId!==first.snapshotId||page.asOf!==first.asOf||JSON.stringify(page.identity)!==JSON.stringify(first.identity)||!Array.isArray(page.items))fail(400,'CHRONOLOGY_EXPORT_SCOPE_MISMATCH','Export pages must belong to one frozen placement read.');
|
|
26
|
+
if(typeof page.hasMore!=='boolean'||(page.hasMore?typeof page.nextCursor!=='string'||!page.nextCursor||page.nextCursor.length>12000:page.nextCursor!==null))fail(400,'INVALID_CHRONOLOGY_EXPORT','Each frozen page must declare whether another page exists with a consistent continuation cursor.');
|
|
27
|
+
if(i&&pages[i-1].nextCursor!==page.pageCursor)continuous=false;
|
|
28
|
+
for(const row of page.items){if(!row||JSON.stringify(row.identity)!==JSON.stringify(first.identity))fail(400,'CHRONOLOGY_EXPORT_SCOPE_MISMATCH','Every retained row must belong to this exact frozen placement.');const previous=byId.get(row.id);if(previous&&JSON.stringify(previous)!==JSON.stringify(row))fail(400,'CHRONOLOGY_EXPORT_CONFLICT','A frozen row changed between pages.');byId.set(row.id,row);}
|
|
29
|
+
}
|
|
30
|
+
for(const row of localItems){if(row.stream!=='local_submissions'||JSON.stringify(row.identity)!==JSON.stringify(first.identity)||!iso(row.sortAt)||row.sortAt>first.asOf)fail(400,'CHRONOLOGY_EXPORT_SCOPE_MISMATCH','Local submission context must match this placement and cutoff.');const previous=byId.get(row.id);if(previous&&JSON.stringify(previous)!==JSON.stringify(row))fail(400,'CHRONOLOGY_EXPORT_CONFLICT','A local receipt identity changed.');byId.set(row.id,row);}
|
|
31
|
+
const complete=continuous&&pages.at(-1).hasMore===false,coverage=JSON.parse(JSON.stringify(pages.at(-1).coverage));
|
|
32
|
+
if(localItems.length)coverage.local_submissions={status:'available',reason:'validated_local_reported_receipts',hasMore:false,providerAuthenticity:'not_established'};
|
|
33
|
+
const items=[...byId.values()].sort(compare),gaps=[];
|
|
34
|
+
for(const [stream,origin] of Object.entries(coverage)){
|
|
35
|
+
if(origin.status==='available'){const loaded=items.filter(row=>row.stream===stream);origin.loadedRange={newestAt:loaded[0]?.sortAt??null,oldestAt:loaded.at(-1)?.sortAt??null,itemCount:loaded.length};}
|
|
36
|
+
if(origin.status!=='available'||origin.compactedBefore||origin.retentionFloor>0)gaps.push({stream,reason:origin.reason,...(origin.compactedBefore?{compactedBefore:origin.compactedBefore}:{}),...(origin.retentionFloor>0?{retentionFloor:origin.retentionFloor}:{})});
|
|
37
|
+
}
|
|
38
|
+
if(!complete)gaps.push({stream:'pagination',reason:'partial_pagination'});
|
|
39
|
+
const data={schemaVersion:1,identity:first.identity,asOf:first.asOf,snapshotId:first.snapshotId,items,coverage,gaps,pagination:{complete,pageCount:pages.length,reason:complete?'all_loaded_pages':'partial_pagination',nextCursor:pages.at(-1).nextCursor??null},historyMeaning:first.historyMeaning};
|
|
40
|
+
const frozen=JSON.parse(JSON.stringify(data));return {...frozen,contentHash:await sha256(JSON.stringify(frozen))};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// This is an offline normalization boundary, not server ingestion, collection or
|
|
44
|
+
// proof of provider authenticity. The hash uses the existing CLI receipt format.
|
|
45
|
+
export async function normalizeLocalSubmissions(rows,identity){
|
|
46
|
+
if(!validIdentity(identity))fail(400,'INVALID_SUBMISSION_RECEIPT','An exact placement identity is required.');
|
|
47
|
+
if(!Array.isArray(rows)||rows.length>10000)fail(400,'INVALID_SUBMISSION_RECEIPT','Provide bounded local submission receipts.');
|
|
48
|
+
const fields=['v','id','project_id','url','channel','endpoint','submitted_at','http_status','evidence_sha256'],items=[],byId=new Map();
|
|
49
|
+
for(const row of rows){
|
|
50
|
+
if(!row||Object.keys(row).some(k=>!fields.includes(k))||fields.some(k=>!Object.hasOwn(row,k))||row.v!==1||!/^[\w-]{1,128}$/.test(row.id??'')||!/^[\w-]{1,128}$/.test(row.project_id??'')||row.channel!=='indexnow'||row.endpoint!=='https://api.indexnow.org/indexnow'||iso(row.submitted_at)!==row.submitted_at||row.http_status!==null&&(!Number.isInteger(row.http_status)||row.http_status<100||row.http_status>599))fail(400,'INVALID_SUBMISSION_RECEIPT','Use the existing validated local submission format.');
|
|
51
|
+
let url;try{url=new URL(row.url);}catch{fail(400,'INVALID_SUBMISSION_RECEIPT','The submission URL is invalid.');}if(url.href!==row.url||!['http:','https:'].includes(url.protocol)||url.username||url.password||url.hash)fail(400,'INVALID_SUBMISSION_RECEIPT','The submission URL must be exact HTTP(S).');
|
|
52
|
+
const body=Object.fromEntries(fields.filter(k=>k!=='evidence_sha256').map(k=>[k,row[k]]));if(await sha256(JSON.stringify(body))!==row.evidence_sha256)fail(400,'INVALID_SUBMISSION_RECEIPT','The local receipt hash does not match.');
|
|
53
|
+
const key=`${row.project_id}:${row.id}`,previous=byId.get(key);if(previous&&JSON.stringify(previous)!==JSON.stringify(row))fail(409,'SUBMISSION_IDENTITY_CONFLICT','A local submission ID has conflicting contents.');byId.set(key,row);if(previous||row.project_id!==identity.projectId||![identity.sourceUrl,identity.targetUrl].includes(row.url))continue;
|
|
54
|
+
const item=baseItem('local_submissions',{id:key},identity,null,null,'reported_submission');items.push({...item,sortAt:row.submitted_at,reason:row.http_status===200?'received':row.http_status===202?'key_validation_pending':row.http_status===null?'unknown':'failed',method:'reported_indexnow_http_response',evidence:{...item.evidence,sha256:row.evidence_sha256,availability:'local_receipt'},data:{...row,subject:row.url===identity.sourceUrl?'source':'target',reportedSubmissionAt:row.submitted_at,sortTimeMeaning:'reported_submission_time',retrievedAt:null,providerAuthenticity:'not_established',participantIndexingVerified:false,causationEstablished:false}});
|
|
55
|
+
}
|
|
56
|
+
return items.sort(compare);
|
|
57
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
const id=z.string().min(1).max(200),time=z.string().datetime(),nullableTime=time.nullable(),count=z.number().int().nonnegative();
|
|
3
|
+
export const chronologyStreams=['context','link_observations','target_observations','link_events','target_events','index_observations','index_events','local_submissions'];
|
|
4
|
+
export const chronologyInput=z.object({watchId:id,limit:z.number().int().min(1).max(100).optional(),cursor:z.string().max(12000).optional(),streams:z.array(z.enum(chronologyStreams)).min(1).max(8).refine(values=>new Set(values).size===values.length).optional()}).strict();
|
|
5
|
+
const identity=z.object({workspaceId:id,projectId:id,watchId:id,sourceUrl:z.url(),targetUrl:z.url(),targetScope:z.enum(['exact','domain','subdomain','path']),targetId:id.nullable(),localReference:z.string().nullable(),matchBasis:z.literal('configured_target_url')});
|
|
6
|
+
export const chronologyResponse=z.object({schemaVersion:z.literal(1),identity,asOf:time,snapshotId:z.string().regex(/^[a-f0-9]{64}$/),items:z.array(z.looseObject({id:z.string(),rawId:z.string(),stream:z.enum(chronologyStreams),kind:z.string(),identity,observedAt:nullableTime,recordedAt:nullableTime,sortAt:time,state:z.string().nullable(),reason:z.string().nullable(),sourceKey:z.string().nullable(),method:z.string().nullable(),checkerVersion:z.string().nullable(),evidence:z.looseObject({sha256:z.string().nullable(),reference:z.string().nullable(),expiresAt:nullableTime,availability:z.string()}),data:z.record(z.string(),z.unknown())})),pageCursor:z.string().nullable(),nextCursor:z.string().nullable(),hasMore:z.boolean(),coverage:z.partialRecord(z.enum(chronologyStreams),z.looseObject({status:z.enum(['available','denied','unavailable']),reason:z.string()})),historyMeaning:z.string()}).passthrough();
|
|
7
|
+
// Additive retained-data projection; absence preserves older backend responses.
|
|
8
|
+
const nullableSeconds=z.number().nonnegative().nullable();
|
|
9
|
+
const coverageProjection=z.object({
|
|
10
|
+
supplied:count,active:count,paused:count,recordedHeld:count,schedulingCandidates:count,busy:count,dueCandidates:count,
|
|
11
|
+
eligibility:z.object({status:z.literal('unavailable'),eligible:z.null(),reason:z.literal('admission_not_rechecked')}),
|
|
12
|
+
denominatorMeaning:z.string(),freshnessWindow:z.string(),cadenceSeconds:z.object({minimum:count.nullable(),maximum:count.nullable()}),
|
|
13
|
+
oldestOverdueAt:nullableTime,oldestOverdueAgeSeconds:nullableSeconds,nextScheduledAt:nullableTime,nextEligibleAt:z.null(),nextEligibleReason:z.literal('admission_not_rechecked'),
|
|
14
|
+
neverAttempted:count,neverConclusive:count,currentUnknown:count,unknownReasons:z.record(z.string(),count),freshConclusive:count,staleConclusive:count,
|
|
15
|
+
shares:z.object({currentUnknown:z.number().min(0).max(1).nullable(),neverAttempted:z.number().min(0).max(1).nullable(),freshConclusive:z.number().min(0).max(1).nullable()}),shareMeaning:z.string(),
|
|
16
|
+
freshRetainedLinkVerification:count.optional(),linkVerificationMeaning:z.string().optional(),reasonCounts:z.object({due:z.record(z.string(),count),deferred:z.record(z.string(),count),unavailable:z.record(z.string(),count).optional()})
|
|
17
|
+
});
|
|
18
|
+
const projectedJobs=z.object({
|
|
19
|
+
retained:count,queued:count,running:count,succeeded:count,failed:count,cancelled:count,everClaimed:count,claimAttempts:count,retryClaims:count,retryExhausted:count,expiredRunningLeases:count,
|
|
20
|
+
oldestQueuedAt:nullableTime,oldestQueuedAgeSeconds:nullableSeconds,oldestRunningAdmissionAt:nullableTime,oldestRunningAdmissionAgeSeconds:nullableSeconds,latestAdmissionAt:nullableTime,latestCompletionAt:nullableTime,
|
|
21
|
+
scopedRetainedDeadLetters:count,deadLetterMeaning:z.string(),admissionToTerminal:z.object({samples:count,minimumSeconds:nullableSeconds,maximumSeconds:nullableSeconds,meaning:z.string()}),
|
|
22
|
+
startedAt:z.null(),queueWaitSeconds:z.null(),providerLatencySeconds:z.null(),timingReason:z.literal('first_start_and_provider_timing_not_retained'),
|
|
23
|
+
dispatch:z.object({outstanding:count,deferred:count,expiredSendingLeases:count,nextDispatchAt:nullableTime,meaning:z.string()}),historyMeaning:z.string()
|
|
24
|
+
});
|
|
25
|
+
const projectedPlane=z.object({coverage:coverageProjection,jobs:projectedJobs});
|
|
26
|
+
const indexProjection=z.discriminatedUnion('status',[
|
|
27
|
+
z.object({status:z.literal('denied'),reason:z.string()}).strict(),z.object({status:z.literal('unavailable'),reason:z.string()}).strict(),
|
|
28
|
+
z.object({status:z.literal('available'),reason:z.string(),suppliedSourceWatches:count,activeSourceWatches:count,
|
|
29
|
+
lanes:z.array(z.object({lane:z.enum(['gsc','licensed_site']),retainedStates:count,observedActiveWatches:count,neverObservedActiveWatches:count,currentUnknownStates:count,latestObservationAt:nullableTime,oldestObservationAt:nullableTime,oldestObservationAgeSeconds:nullableSeconds})).length(2),
|
|
30
|
+
schedule:z.discriminatedUnion('status',[z.object({status:z.literal('unavailable'),reason:z.literal('schema_unavailable')}),z.object({status:z.literal('available'),projectOptedIn:z.boolean(),inspectionEnabled:z.boolean(),schedulerEnabled:z.boolean(),cadenceSeconds:count.nullable(),dailyLimit:count.nullable(),nextProjectScanAt:nullableTime,propertyEligibleWatches:z.null(),propertyEligibilityReason:z.literal('current_source_property_grant_not_assessed'),eligibleWatches:z.null(),eligibilityReason:z.literal('admission_not_rechecked')})]),meaning:z.string()})
|
|
31
|
+
]);
|
|
32
|
+
export const monitoringAssuranceProjection=z.object({schemaVersion:z.literal(1),source:projectedPlane,target:projectedPlane,index:indexProjection,
|
|
33
|
+
serviceTickWindow:z.object({expectedTicks:z.null(),completedTicks:z.null(),availabilityShare:z.null(),reason:z.literal('tick_history_not_retained')}),
|
|
34
|
+
capacity:z.object({dueOpportunities:z.null(),missedOpportunities:z.null(),checksPerEligibleWatch:z.null(),reason:z.literal('bounded_roster_opportunity_history_not_retained')}),meaning:z.string()});
|
|
35
|
+
export const assuranceInput=z.object({projectId:id}).strict();
|
|
36
|
+
const metric=z.object({supplied:count,active:count,paused:count,neverAttempted:count,neverConclusive:count,due:count,overdue:count,currentUnknown:count,latestAttemptAt:nullableTime,oldestAttemptAt:nullableTime,oldestAttemptAgeSeconds:z.number().nonnegative().nullable(),latestConclusiveAt:nullableTime,oldestConclusiveAt:nullableTime,oldestConclusiveAgeSeconds:z.number().nonnegative().nullable()});
|
|
37
|
+
export const assuranceResponse=z.object({schemaVersion:z.literal(1),workspaceId:id,projectId:id,asOf:time,source:metric,target:metric,monitoringProjection:monitoringAssuranceProjection.optional(),sourceLinkVerification:z.object({latestRetainedAt:nullableTime,oldestRetainedAt:nullableTime,observedWatches:count,meaning:z.string()}),sourceJobs:z.object({queued:count,running:count}),overdueReasons:z.record(z.string(),count),index:z.discriminatedUnion('status',[z.object({status:z.literal('available'),reason:z.string(),observedWatches:count,latestObservationAt:nullableTime,oldestObservationAt:nullableTime,oldestObservationAgeSeconds:z.number().nonnegative().nullable(),denominator:z.string()}),z.object({status:z.literal('denied'),reason:z.string()}),z.object({status:z.literal('unavailable'),reason:z.string()})]),serviceTicks:z.object({status:z.enum(['available','unavailable']),meaning:z.string(),items:z.array(z.object({plan:z.string(),latestAttemptAt:time,latestCompletionAt:nullableTime,latestSuccessAt:nullableTime,latestOutcome:z.enum(['running','succeeded','failed']),ageSeconds:z.number().nonnegative()}))}),assurance:z.object({elapsedSoak:z.literal('not_established'),backupRestore:z.literal('not_established'),externalDelivery:z.literal('not_established')}),denominator:z.string()}).passthrough();
|
|
38
|
+
export const monitoringReadResponses={get_placement_chronology:chronologyResponse,get_monitoring_service_assurance:assuranceResponse};
|
|
39
|
+
const at='2026-10-07T00:00:00.000Z',projectId='pr_example',workspaceId='ws_example';
|
|
40
|
+
const emptyMetric={supplied:0,active:0,paused:0,neverAttempted:0,neverConclusive:0,due:0,overdue:0,currentUnknown:0,latestAttemptAt:null,oldestAttemptAt:null,oldestAttemptAgeSeconds:null,latestConclusiveAt:null,oldestConclusiveAt:null,oldestConclusiveAgeSeconds:null};
|
|
41
|
+
export const monitoringReadExamples={get_placement_chronology:{schemaVersion:1,identity:{workspaceId,projectId,watchId:'watch_example',sourceUrl:'https://publisher.example.org/resource',targetUrl:'https://example.org/guide',targetScope:'exact',targetId:null,localReference:null,matchBasis:'configured_target_url'},asOf:at,snapshotId:'a'.repeat(64),items:[],pageCursor:null,nextCursor:null,hasMore:false,coverage:{context:{status:'available',reason:'recorded_declarations_only'},local_submissions:{status:'unavailable',reason:'local_only_not_hosted'}},historyMeaning:'Retained evidence only; gaps do not establish inactivity.'},get_monitoring_service_assurance:{schemaVersion:1,workspaceId,projectId,asOf:at,source:emptyMetric,target:emptyMetric,sourceLinkVerification:{latestRetainedAt:null,oldestRetainedAt:null,observedWatches:0,meaning:'Retained complete link receipts.'},sourceJobs:{queued:0,running:0},overdueReasons:{},index:{status:'unavailable',reason:'not_configured'},serviceTicks:{status:'unavailable',meaning:'Scheduler metadata, not customer coverage.',items:[]},assurance:{elapsedSoak:'not_established',backupRestore:'not_established',externalDelivery:'not_established'},denominator:'Supplied project records; zero is not an inventory of all backlinks.'}};
|
|
File without changes
|
|
File without changes
|