diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index e8c9b92..267f339 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentlinkops", - "version": "0.6.12", + "version": "0.6.13", "description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.", "author": { "name": "AgentLinkOps" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index ff322fe..6fffa51 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentlinkops", - "version": "0.6.12", + "version": "0.6.13", "description": "Connect an agent to AgentLinkOps and run backlink work with it: the connect skill (views, the three discovery verbs, evidence rules), local ledger and campaign skills, a portable CRM, the source-cited content pipeline, user-managed disavow lists, HTML reports and monitoring-event sync.", "author": { "name": "AgentLinkOps" diff --git a/CHANGELOG.md b/CHANGELOG.md index 70f0a60..47311c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the `agentlinkops` package are recorded here. The version follows the plugin manifests in `.claude-plugin/` and `.codex-plugin/`. +## 0.6.13 (unreleased) + +- Show the latest attempt, conclusive source check and link verification separately in status, fleet and reports. Unknown checks retain the previous link verification for the same placement. +- Add `chronology pull` for saved placement history on compatible hosted versions and `chronology export` for frozen local pages. Exports retain denied origins, history gaps and partial paging. +- Preserve existing frozen version-two reports and reject exports that would replace an input or existing file. + ## 0.6.12 (2026-10-07) - Check every watch import result before updating sync mappings or reporting progress. Reject incomplete, duplicate or out-of-range results without resending the request. diff --git a/cli/catalog.js b/cli/catalog.js index 676742b..0507bdd 100644 --- a/cli/catalog.js +++ b/cli/catalog.js @@ -9,11 +9,12 @@ export const CATALOG_TOOLSETS = { "monitoring": "Watch earned links and destination URLs: create, list, update, import, export, recheck.", "evidence": "What a check observed: histories, snapshots, change feeds, check jobs, published contacts.", "rank": "Synthetic DataForSEO rank schedules and dated history; hosted collection disabled.", + "licensed": "Licensed source-search evidence and charge custody; operator admission required.", "discovery": "Import candidate rows, read stored runs, verify selected candidates, enroll them.", "baseline": "Scoped backlink baselines; supplier admission disabled.", "library": "Read and export the coverage-stated opportunity library.", "competitors": "Competitor sets, dated inventories, scheduled refresh, gap and domain-mix reports.", - "reports": "Profile, anchor and report summaries over the tracked dataset.", + "reports": "Placement chronology, service assurance and backlink reports.", "workspace": "Workspace, projects, usage, members, invitations, scratch-resource cleanup.", "notifications": "Email notification preferences, previews, tests and deliveries.", "webhooks": "Webhook endpoints, state, secrets and delivery records." @@ -21099,6 +21100,4233 @@ export const CATALOG_COMMANDS = [ "additionalProperties": {} } }, + { + "name": "configure_licensed_index_observations", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:write", + "watches:write" + ], + "readOnly": false, + "description": "Configure the initial US/en licensed source-search observation lane for one project. Activation and supplier funding require separate operator admission.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + }, + "enabled": { + "type": "boolean", + "description": "The enabled value; allowed values and bounds are specified in this schema." + }, + "locationCode": { + "type": "number", + "const": 2840, + "description": "The location code value; allowed values and bounds are specified in this schema." + }, + "language": { + "type": "string", + "const": "en", + "description": "The language value; allowed values and bounds are specified in this schema." + }, + "country": { + "type": "string", + "const": "US", + "description": "The country value; allowed values and bounds are specified in this schema." + }, + "dailyLimit": { + "description": "The daily limit value; allowed values and bounds are specified in this schema.", + "type": "integer", + "minimum": 1, + "maximum": 1000 + }, + "retentionDays": { + "description": "The retention days value; allowed values and bounds are specified in this schema.", + "type": "integer", + "minimum": 1, + "maximum": 90 + }, + "expectedRevision": { + "description": "Revision last read by the caller; mismatches reject concurrent updates.", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "projectId", + "enabled", + "locationCode", + "language", + "country" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "settings": { + "anyOf": [ + { + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "locationCode": { + "type": "number", + "const": 2840 + }, + "language": { + "type": "string", + "const": "en" + }, + "country": { + "type": "string", + "const": "US" + }, + "dailyLimit": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "retentionDays": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + } + }, + "required": [ + "projectId", + "enabled", + "revision", + "locationCode", + "language", + "country", + "dailyLimit", + "retentionDays", + "updatedAt" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "transportEnabled": { + "type": "boolean" + } + }, + "required": [ + "projectId", + "settings", + "transportEnabled" + ], + "additionalProperties": {} + } + }, + { + "name": "get_licensed_index_settings", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:read", + "watches:read" + ], + "readOnly": true, + "description": "Read project opt-in and transport configuration for licensed source-search observations. Configuration does not establish spending admission.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + } + }, + "required": [ + "projectId" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "settings": { + "anyOf": [ + { + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "locationCode": { + "type": "number", + "const": 2840 + }, + "language": { + "type": "string", + "const": "en" + }, + "country": { + "type": "string", + "const": "US" + }, + "dailyLimit": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "retentionDays": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + } + }, + "required": [ + "projectId", + "enabled", + "revision", + "locationCode", + "language", + "country", + "dailyLimit", + "retentionDays", + "updatedAt" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "transportEnabled": { + "type": "boolean" + } + }, + "required": [ + "projectId", + "settings", + "transportEnabled" + ], + "additionalProperties": {} + } + }, + { + "name": "inspect_licensed_index_status", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:write", + "watches:write" + ], + "readOnly": false, + "description": "Request one bounded licensed Google site-query observation for a monitored publisher URL. Sampled absence never establishes confirmed deindexing.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the watch returned by its create or list operation." + }, + "idempotencyKey": { + "type": "string", + "pattern": "^[\\x21-\\x7e]{1,128}$", + "description": "Caller-generated key reused only for retries of the same operation and arguments in this workspace." + } + }, + "required": [ + "projectId", + "watchId", + "idempotencyKey" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "request": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "completed", + "uncertain", + "cancelled" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "dispatchedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "cancelledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "providerTaskId": { + "type": [ + "string", + "null" + ] + }, + "errorCode": { + "type": [ + "string", + "null" + ] + }, + "request": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "requestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "responseSha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "observation": { + "anyOf": [ + { + "type": "object", + "properties": { + "observation_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "checked_at": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "tier": { + "type": "string", + "enum": [ + "likely_indexed", + "not_found_in_site_query", + "unknown" + ] + }, + "confidence": { + "type": "string", + "enum": [ + "search_result_sample", + "unknown" + ] + }, + "reason": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "source_key": { + "type": "string" + } + }, + "required": [ + "observation_id", + "checked_at", + "tier", + "confidence", + "reason", + "url", + "source_key" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "evidenceAvailable": { + "type": "boolean" + }, + "rawResponse": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "cost": { + "type": "object", + "properties": { + "ceilingMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "actualMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "ambiguous", + "consumed", + "released", + "unavailable" + ] + } + }, + "required": [ + "ceilingMicrousd", + "actualMicrousd", + "state" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "projectId", + "watchId", + "state", + "createdAt", + "dispatchedAt", + "completedAt", + "cancelledAt", + "providerTaskId", + "errorCode", + "request", + "requestSha256", + "responseSha256", + "observation", + "evidenceAvailable", + "rawResponse", + "cost" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "indexProjection": { + "type": "object", + "properties": { + "receiptId": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "retained", + "metadata_only", + "not_retained", + "not_projected", + "projection_unavailable" + ] + } + }, + "required": [ + "receiptId", + "status" + ], + "additionalProperties": {} + } + }, + "required": [ + "request", + "indexProjection" + ], + "additionalProperties": {} + } + }, + { + "name": "get_licensed_index_request", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:read", + "watches:read" + ], + "readOnly": true, + "description": "Read a dated licensed source-search request, retained snapshot and cost settlement. This read performs no supplier call.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the watch returned by its create or list operation." + }, + "requestId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the request returned by its create or list operation." + } + }, + "required": [ + "projectId", + "watchId", + "requestId" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "request": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "completed", + "uncertain", + "cancelled" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "dispatchedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "cancelledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "providerTaskId": { + "type": [ + "string", + "null" + ] + }, + "errorCode": { + "type": [ + "string", + "null" + ] + }, + "request": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "requestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "responseSha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "observation": { + "anyOf": [ + { + "type": "object", + "properties": { + "observation_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "checked_at": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "tier": { + "type": "string", + "enum": [ + "likely_indexed", + "not_found_in_site_query", + "unknown" + ] + }, + "confidence": { + "type": "string", + "enum": [ + "search_result_sample", + "unknown" + ] + }, + "reason": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "source_key": { + "type": "string" + } + }, + "required": [ + "observation_id", + "checked_at", + "tier", + "confidence", + "reason", + "url", + "source_key" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "evidenceAvailable": { + "type": "boolean" + }, + "rawResponse": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "cost": { + "type": "object", + "properties": { + "ceilingMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "actualMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "ambiguous", + "consumed", + "released", + "unavailable" + ] + } + }, + "required": [ + "ceilingMicrousd", + "actualMicrousd", + "state" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "projectId", + "watchId", + "state", + "createdAt", + "dispatchedAt", + "completedAt", + "cancelledAt", + "providerTaskId", + "errorCode", + "request", + "requestSha256", + "responseSha256", + "observation", + "evidenceAvailable", + "rawResponse", + "cost" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "indexProjection": { + "type": "object", + "properties": { + "receiptId": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "retained", + "metadata_only", + "not_retained", + "not_projected", + "projection_unavailable" + ] + } + }, + "required": [ + "receiptId", + "status" + ], + "additionalProperties": {} + } + }, + "required": [ + "request", + "indexProjection" + ], + "additionalProperties": {} + } + }, + { + "name": "cancel_licensed_index_request", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:write", + "watches:write" + ], + "readOnly": false, + "description": "Cancel an undispatched licensed-search reservation or hold an already dispatched uncertain effect. Cancellation does not prove a remote task was cancelled.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the watch returned by its create or list operation." + }, + "requestId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the request returned by its create or list operation." + } + }, + "required": [ + "projectId", + "watchId", + "requestId" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "request": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "completed", + "uncertain", + "cancelled" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "dispatchedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "cancelledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "providerTaskId": { + "type": [ + "string", + "null" + ] + }, + "errorCode": { + "type": [ + "string", + "null" + ] + }, + "request": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "requestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "responseSha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "observation": { + "anyOf": [ + { + "type": "object", + "properties": { + "observation_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "checked_at": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "tier": { + "type": "string", + "enum": [ + "likely_indexed", + "not_found_in_site_query", + "unknown" + ] + }, + "confidence": { + "type": "string", + "enum": [ + "search_result_sample", + "unknown" + ] + }, + "reason": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "source_key": { + "type": "string" + } + }, + "required": [ + "observation_id", + "checked_at", + "tier", + "confidence", + "reason", + "url", + "source_key" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "evidenceAvailable": { + "type": "boolean" + }, + "rawResponse": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "cost": { + "type": "object", + "properties": { + "ceilingMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "actualMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "ambiguous", + "consumed", + "released", + "unavailable" + ] + } + }, + "required": [ + "ceilingMicrousd", + "actualMicrousd", + "state" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "projectId", + "watchId", + "state", + "createdAt", + "dispatchedAt", + "completedAt", + "cancelledAt", + "providerTaskId", + "errorCode", + "request", + "requestSha256", + "responseSha256", + "observation", + "evidenceAvailable", + "rawResponse", + "cost" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "indexProjection": { + "type": "object", + "properties": { + "receiptId": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "retained", + "metadata_only", + "not_retained", + "not_projected", + "projection_unavailable" + ] + } + }, + "required": [ + "receiptId", + "status" + ], + "additionalProperties": {} + } + }, + "required": [ + "request", + "indexProjection" + ], + "additionalProperties": {} + } + }, + { + "name": "reconcile_licensed_index_request", + "toolset": "licensed", + "tier": "deferred", + "aliases": [], + "keywords": [ + "licensed index", + "site query", + "dataforseo" + ], + "confirm": "none", + "admissionGated": false, + "scopes": [ + "discovery:write", + "watches:write" + ], + "readOnly": false, + "description": "Reconcile an uncertain licensed-search effect using an operator-trusted supplier receipt. The original provider request is never resubmitted.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the watch returned by its create or list operation." + }, + "requestId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the request returned by its create or list operation." + }, + "receiptReference": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "The receipt reference value; allowed values and bounds are specified in this schema." + } + }, + "required": [ + "projectId", + "watchId", + "requestId", + "receiptReference" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "request": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "completed", + "uncertain", + "cancelled" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "dispatchedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "completedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "cancelledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "providerTaskId": { + "type": [ + "string", + "null" + ] + }, + "errorCode": { + "type": [ + "string", + "null" + ] + }, + "request": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "requestSha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "responseSha256": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "observation": { + "anyOf": [ + { + "type": "object", + "properties": { + "observation_id": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "checked_at": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "tier": { + "type": "string", + "enum": [ + "likely_indexed", + "not_found_in_site_query", + "unknown" + ] + }, + "confidence": { + "type": "string", + "enum": [ + "search_result_sample", + "unknown" + ] + }, + "reason": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "source_key": { + "type": "string" + } + }, + "required": [ + "observation_id", + "checked_at", + "tier", + "confidence", + "reason", + "url", + "source_key" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "evidenceAvailable": { + "type": "boolean" + }, + "rawResponse": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "cost": { + "type": "object", + "properties": { + "ceilingMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "actualMicrousd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "state": { + "type": "string", + "enum": [ + "reserved", + "dispatched", + "ambiguous", + "consumed", + "released", + "unavailable" + ] + } + }, + "required": [ + "ceilingMicrousd", + "actualMicrousd", + "state" + ], + "additionalProperties": false + } + }, + "required": [ + "id", + "projectId", + "watchId", + "state", + "createdAt", + "dispatchedAt", + "completedAt", + "cancelledAt", + "providerTaskId", + "errorCode", + "request", + "requestSha256", + "responseSha256", + "observation", + "evidenceAvailable", + "rawResponse", + "cost" + ], + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "indexProjection": { + "type": "object", + "properties": { + "receiptId": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "retained", + "metadata_only", + "not_retained", + "not_projected", + "projection_unavailable" + ] + } + }, + "required": [ + "receiptId", + "status" + ], + "additionalProperties": {} + } + }, + "required": [ + "request", + "indexProjection" + ], + "additionalProperties": {} + } + }, + { + "name": "get_placement_chronology", + "toolset": "reports", + "tier": "deferred", + "aliases": [], + "keywords": [ + "placement history", + "chronology", + "timeline", + "export" + ], + "admissionGated": false, + "scopes": [ + "watches:read" + ], + "readOnly": true, + "description": "Read one monitored placement’s retained source, target and index history with frozen continuation and explicit evidence gaps. This read performs no new checks.", + "examples": [ + { + "watchId": "watch_example", + "limit": 50 + } + ], + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the watch returned by its create or list operation." + }, + "limit": { + "description": "Maximum rows in this page or bounded report; subject to the schema maximum.", + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "cursor": { + "description": "Opaque continuation returned by this same listing; omit for the first page.", + "type": "string", + "maxLength": 12000 + }, + "streams": { + "description": "The streams value; allowed values and bounds are specified in this schema.", + "minItems": 1, + "maxItems": 8, + "type": "array", + "items": { + "type": "string", + "enum": [ + "context", + "link_observations", + "target_observations", + "link_events", + "target_events", + "index_observations", + "index_events", + "local_submissions" + ] + } + } + }, + "required": [ + "watchId" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "identity": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "sourceUrl": { + "type": "string", + "format": "uri" + }, + "targetUrl": { + "type": "string", + "format": "uri" + }, + "targetScope": { + "type": "string", + "enum": [ + "exact", + "domain", + "subdomain", + "path" + ] + }, + "targetId": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ] + }, + "localReference": { + "type": [ + "string", + "null" + ] + }, + "matchBasis": { + "type": "string", + "const": "configured_target_url" + } + }, + "required": [ + "workspaceId", + "projectId", + "watchId", + "sourceUrl", + "targetUrl", + "targetScope", + "targetId", + "localReference", + "matchBasis" + ], + "additionalProperties": false + }, + "asOf": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "snapshotId": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "rawId": { + "type": "string" + }, + "stream": { + "type": "string", + "enum": [ + "context", + "link_observations", + "target_observations", + "link_events", + "target_events", + "index_observations", + "index_events", + "local_submissions" + ] + }, + "kind": { + "type": "string" + }, + "identity": { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "watchId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "sourceUrl": { + "type": "string", + "format": "uri" + }, + "targetUrl": { + "type": "string", + "format": "uri" + }, + "targetScope": { + "type": "string", + "enum": [ + "exact", + "domain", + "subdomain", + "path" + ] + }, + "targetId": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + { + "type": "null" + } + ] + }, + "localReference": { + "type": [ + "string", + "null" + ] + }, + "matchBasis": { + "type": "string", + "const": "configured_target_url" + } + }, + "required": [ + "workspaceId", + "projectId", + "watchId", + "sourceUrl", + "targetUrl", + "targetScope", + "targetId", + "localReference", + "matchBasis" + ], + "additionalProperties": false + }, + "observedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "recordedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "sortAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "state": { + "type": [ + "string", + "null" + ] + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "sourceKey": { + "type": [ + "string", + "null" + ] + }, + "method": { + "type": [ + "string", + "null" + ] + }, + "checkerVersion": { + "type": [ + "string", + "null" + ] + }, + "evidence": { + "type": "object", + "properties": { + "sha256": { + "type": [ + "string", + "null" + ] + }, + "reference": { + "type": [ + "string", + "null" + ] + }, + "expiresAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "availability": { + "type": "string" + } + }, + "required": [ + "sha256", + "reference", + "expiresAt", + "availability" + ], + "additionalProperties": {} + }, + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "id", + "rawId", + "stream", + "kind", + "identity", + "observedAt", + "recordedAt", + "sortAt", + "state", + "reason", + "sourceKey", + "method", + "checkerVersion", + "evidence", + "data" + ], + "additionalProperties": {} + } + }, + "pageCursor": { + "type": [ + "string", + "null" + ] + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + }, + "hasMore": { + "type": "boolean" + }, + "coverage": { + "type": "object", + "propertyNames": { + "type": "string", + "enum": [ + "context", + "link_observations", + "target_observations", + "link_events", + "target_events", + "index_observations", + "index_events", + "local_submissions" + ] + }, + "additionalProperties": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "available", + "denied", + "unavailable" + ] + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": {} + } + }, + "historyMeaning": { + "type": "string" + } + }, + "required": [ + "schemaVersion", + "identity", + "asOf", + "snapshotId", + "items", + "pageCursor", + "nextCursor", + "hasMore", + "coverage", + "historyMeaning" + ], + "additionalProperties": {} + } + }, + { + "name": "get_monitoring_service_assurance", + "toolset": "reports", + "tier": "deferred", + "aliases": [], + "keywords": [ + "freshness", + "coverage", + "service health" + ], + "admissionGated": false, + "scopes": [ + "watches:read" + ], + "readOnly": true, + "description": "Read supplied-record coverage, freshness and scheduler availability for one project. Unknown evidence and unproved delivery or soak remain explicit.", + "examples": [ + { + "projectId": "pr_example" + } + ], + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Identifier of the project returned by its create or list operation." + } + }, + "required": [ + "projectId" + ], + "additionalProperties": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "projectId": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "asOf": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "source": { + "type": "object", + "properties": { + "supplied": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "active": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "paused": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverAttempted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "due": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "overdue": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "currentUnknown": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "latestAttemptAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestAttemptAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestAttemptAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "latestConclusiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestConclusiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestConclusiveAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "supplied", + "active", + "paused", + "neverAttempted", + "neverConclusive", + "due", + "overdue", + "currentUnknown", + "latestAttemptAt", + "oldestAttemptAt", + "oldestAttemptAgeSeconds", + "latestConclusiveAt", + "oldestConclusiveAt", + "oldestConclusiveAgeSeconds" + ], + "additionalProperties": false + }, + "target": { + "type": "object", + "properties": { + "supplied": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "active": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "paused": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverAttempted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "due": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "overdue": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "currentUnknown": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "latestAttemptAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestAttemptAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestAttemptAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "latestConclusiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestConclusiveAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestConclusiveAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "supplied", + "active", + "paused", + "neverAttempted", + "neverConclusive", + "due", + "overdue", + "currentUnknown", + "latestAttemptAt", + "oldestAttemptAt", + "oldestAttemptAgeSeconds", + "latestConclusiveAt", + "oldestConclusiveAt", + "oldestConclusiveAgeSeconds" + ], + "additionalProperties": false + }, + "monitoringProjection": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "source": { + "type": "object", + "properties": { + "coverage": { + "type": "object", + "properties": { + "supplied": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "active": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "paused": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "recordedHeld": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "schedulingCandidates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "busy": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "dueCandidates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "eligibility": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "unavailable" + }, + "eligible": { + "type": "null" + }, + "reason": { + "type": "string", + "const": "admission_not_rechecked" + } + }, + "required": [ + "status", + "eligible", + "reason" + ], + "additionalProperties": false + }, + "denominatorMeaning": { + "type": "string" + }, + "freshnessWindow": { + "type": "string" + }, + "cadenceSeconds": { + "type": "object", + "properties": { + "minimum": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "minimum", + "maximum" + ], + "additionalProperties": false + }, + "oldestOverdueAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestOverdueAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "nextScheduledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "nextEligibleAt": { + "type": "null" + }, + "nextEligibleReason": { + "type": "string", + "const": "admission_not_rechecked" + }, + "neverAttempted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "currentUnknown": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "unknownReasons": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "freshConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "staleConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "shares": { + "type": "object", + "properties": { + "currentUnknown": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + }, + "neverAttempted": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + }, + "freshConclusive": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "currentUnknown", + "neverAttempted", + "freshConclusive" + ], + "additionalProperties": false + }, + "shareMeaning": { + "type": "string" + }, + "freshRetainedLinkVerification": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "linkVerificationMeaning": { + "type": "string" + }, + "reasonCounts": { + "type": "object", + "properties": { + "due": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "deferred": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "unavailable": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + } + }, + "required": [ + "due", + "deferred" + ], + "additionalProperties": false + } + }, + "required": [ + "supplied", + "active", + "paused", + "recordedHeld", + "schedulingCandidates", + "busy", + "dueCandidates", + "eligibility", + "denominatorMeaning", + "freshnessWindow", + "cadenceSeconds", + "oldestOverdueAt", + "oldestOverdueAgeSeconds", + "nextScheduledAt", + "nextEligibleAt", + "nextEligibleReason", + "neverAttempted", + "neverConclusive", + "currentUnknown", + "unknownReasons", + "freshConclusive", + "staleConclusive", + "shares", + "shareMeaning", + "reasonCounts" + ], + "additionalProperties": false + }, + "jobs": { + "type": "object", + "properties": { + "retained": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "queued": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "running": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "succeeded": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "failed": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "cancelled": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "everClaimed": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "claimAttempts": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "retryClaims": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "retryExhausted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiredRunningLeases": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "oldestQueuedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestQueuedAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "oldestRunningAdmissionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestRunningAdmissionAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "latestAdmissionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "latestCompletionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "scopedRetainedDeadLetters": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "deadLetterMeaning": { + "type": "string" + }, + "admissionToTerminal": { + "type": "object", + "properties": { + "samples": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "minimumSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "maximumSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "samples", + "minimumSeconds", + "maximumSeconds", + "meaning" + ], + "additionalProperties": false + }, + "startedAt": { + "type": "null" + }, + "queueWaitSeconds": { + "type": "null" + }, + "providerLatencySeconds": { + "type": "null" + }, + "timingReason": { + "type": "string", + "const": "first_start_and_provider_timing_not_retained" + }, + "dispatch": { + "type": "object", + "properties": { + "outstanding": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "deferred": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiredSendingLeases": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "nextDispatchAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "outstanding", + "deferred", + "expiredSendingLeases", + "nextDispatchAt", + "meaning" + ], + "additionalProperties": false + }, + "historyMeaning": { + "type": "string" + } + }, + "required": [ + "retained", + "queued", + "running", + "succeeded", + "failed", + "cancelled", + "everClaimed", + "claimAttempts", + "retryClaims", + "retryExhausted", + "expiredRunningLeases", + "oldestQueuedAt", + "oldestQueuedAgeSeconds", + "oldestRunningAdmissionAt", + "oldestRunningAdmissionAgeSeconds", + "latestAdmissionAt", + "latestCompletionAt", + "scopedRetainedDeadLetters", + "deadLetterMeaning", + "admissionToTerminal", + "startedAt", + "queueWaitSeconds", + "providerLatencySeconds", + "timingReason", + "dispatch", + "historyMeaning" + ], + "additionalProperties": false + } + }, + "required": [ + "coverage", + "jobs" + ], + "additionalProperties": false + }, + "target": { + "type": "object", + "properties": { + "coverage": { + "type": "object", + "properties": { + "supplied": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "active": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "paused": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "recordedHeld": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "schedulingCandidates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "busy": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "dueCandidates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "eligibility": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "unavailable" + }, + "eligible": { + "type": "null" + }, + "reason": { + "type": "string", + "const": "admission_not_rechecked" + } + }, + "required": [ + "status", + "eligible", + "reason" + ], + "additionalProperties": false + }, + "denominatorMeaning": { + "type": "string" + }, + "freshnessWindow": { + "type": "string" + }, + "cadenceSeconds": { + "type": "object", + "properties": { + "minimum": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "maximum": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "minimum", + "maximum" + ], + "additionalProperties": false + }, + "oldestOverdueAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestOverdueAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "nextScheduledAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "nextEligibleAt": { + "type": "null" + }, + "nextEligibleReason": { + "type": "string", + "const": "admission_not_rechecked" + }, + "neverAttempted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "currentUnknown": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "unknownReasons": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "freshConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "staleConclusive": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "shares": { + "type": "object", + "properties": { + "currentUnknown": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + }, + "neverAttempted": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + }, + "freshConclusive": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "currentUnknown", + "neverAttempted", + "freshConclusive" + ], + "additionalProperties": false + }, + "shareMeaning": { + "type": "string" + }, + "freshRetainedLinkVerification": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "linkVerificationMeaning": { + "type": "string" + }, + "reasonCounts": { + "type": "object", + "properties": { + "due": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "deferred": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "unavailable": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + } + }, + "required": [ + "due", + "deferred" + ], + "additionalProperties": false + } + }, + "required": [ + "supplied", + "active", + "paused", + "recordedHeld", + "schedulingCandidates", + "busy", + "dueCandidates", + "eligibility", + "denominatorMeaning", + "freshnessWindow", + "cadenceSeconds", + "oldestOverdueAt", + "oldestOverdueAgeSeconds", + "nextScheduledAt", + "nextEligibleAt", + "nextEligibleReason", + "neverAttempted", + "neverConclusive", + "currentUnknown", + "unknownReasons", + "freshConclusive", + "staleConclusive", + "shares", + "shareMeaning", + "reasonCounts" + ], + "additionalProperties": false + }, + "jobs": { + "type": "object", + "properties": { + "retained": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "queued": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "running": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "succeeded": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "failed": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "cancelled": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "everClaimed": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "claimAttempts": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "retryClaims": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "retryExhausted": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiredRunningLeases": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "oldestQueuedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestQueuedAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "oldestRunningAdmissionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestRunningAdmissionAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "latestAdmissionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "latestCompletionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "scopedRetainedDeadLetters": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "deadLetterMeaning": { + "type": "string" + }, + "admissionToTerminal": { + "type": "object", + "properties": { + "samples": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "minimumSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "maximumSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "samples", + "minimumSeconds", + "maximumSeconds", + "meaning" + ], + "additionalProperties": false + }, + "startedAt": { + "type": "null" + }, + "queueWaitSeconds": { + "type": "null" + }, + "providerLatencySeconds": { + "type": "null" + }, + "timingReason": { + "type": "string", + "const": "first_start_and_provider_timing_not_retained" + }, + "dispatch": { + "type": "object", + "properties": { + "outstanding": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "deferred": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiredSendingLeases": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "nextDispatchAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "outstanding", + "deferred", + "expiredSendingLeases", + "nextDispatchAt", + "meaning" + ], + "additionalProperties": false + }, + "historyMeaning": { + "type": "string" + } + }, + "required": [ + "retained", + "queued", + "running", + "succeeded", + "failed", + "cancelled", + "everClaimed", + "claimAttempts", + "retryClaims", + "retryExhausted", + "expiredRunningLeases", + "oldestQueuedAt", + "oldestQueuedAgeSeconds", + "oldestRunningAdmissionAt", + "oldestRunningAdmissionAgeSeconds", + "latestAdmissionAt", + "latestCompletionAt", + "scopedRetainedDeadLetters", + "deadLetterMeaning", + "admissionToTerminal", + "startedAt", + "queueWaitSeconds", + "providerLatencySeconds", + "timingReason", + "dispatch", + "historyMeaning" + ], + "additionalProperties": false + } + }, + "required": [ + "coverage", + "jobs" + ], + "additionalProperties": false + }, + "index": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "denied" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "unavailable" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "available" + }, + "reason": { + "type": "string" + }, + "suppliedSourceWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "activeSourceWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "lanes": { + "minItems": 2, + "maxItems": 2, + "type": "array", + "items": { + "type": "object", + "properties": { + "lane": { + "type": "string", + "enum": [ + "gsc", + "licensed_site" + ] + }, + "retainedStates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "observedActiveWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "neverObservedActiveWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "currentUnknownStates": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "latestObservationAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestObservationAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestObservationAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "lane", + "retainedStates", + "observedActiveWatches", + "neverObservedActiveWatches", + "currentUnknownStates", + "latestObservationAt", + "oldestObservationAt", + "oldestObservationAgeSeconds" + ], + "additionalProperties": false + } + }, + "schedule": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "unavailable" + }, + "reason": { + "type": "string", + "const": "schema_unavailable" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "available" + }, + "projectOptedIn": { + "type": "boolean" + }, + "inspectionEnabled": { + "type": "boolean" + }, + "schedulerEnabled": { + "type": "boolean" + }, + "cadenceSeconds": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "dailyLimit": { + "anyOf": [ + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "nextProjectScanAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "propertyEligibleWatches": { + "type": "null" + }, + "propertyEligibilityReason": { + "type": "string", + "const": "current_source_property_grant_not_assessed" + }, + "eligibleWatches": { + "type": "null" + }, + "eligibilityReason": { + "type": "string", + "const": "admission_not_rechecked" + } + }, + "required": [ + "status", + "projectOptedIn", + "inspectionEnabled", + "schedulerEnabled", + "cadenceSeconds", + "dailyLimit", + "nextProjectScanAt", + "propertyEligibleWatches", + "propertyEligibilityReason", + "eligibleWatches", + "eligibilityReason" + ], + "additionalProperties": false + } + ] + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "status", + "reason", + "suppliedSourceWatches", + "activeSourceWatches", + "lanes", + "schedule", + "meaning" + ], + "additionalProperties": false + } + ] + }, + "serviceTickWindow": { + "type": "object", + "properties": { + "expectedTicks": { + "type": "null" + }, + "completedTicks": { + "type": "null" + }, + "availabilityShare": { + "type": "null" + }, + "reason": { + "type": "string", + "const": "tick_history_not_retained" + } + }, + "required": [ + "expectedTicks", + "completedTicks", + "availabilityShare", + "reason" + ], + "additionalProperties": false + }, + "capacity": { + "type": "object", + "properties": { + "dueOpportunities": { + "type": "null" + }, + "missedOpportunities": { + "type": "null" + }, + "checksPerEligibleWatch": { + "type": "null" + }, + "reason": { + "type": "string", + "const": "bounded_roster_opportunity_history_not_retained" + } + }, + "required": [ + "dueOpportunities", + "missedOpportunities", + "checksPerEligibleWatch", + "reason" + ], + "additionalProperties": false + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "schemaVersion", + "source", + "target", + "index", + "serviceTickWindow", + "capacity", + "meaning" + ], + "additionalProperties": false + }, + "sourceLinkVerification": { + "type": "object", + "properties": { + "latestRetainedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestRetainedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "observedWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "meaning": { + "type": "string" + } + }, + "required": [ + "latestRetainedAt", + "oldestRetainedAt", + "observedWatches", + "meaning" + ], + "additionalProperties": false + }, + "sourceJobs": { + "type": "object", + "properties": { + "queued": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "running": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "queued", + "running" + ], + "additionalProperties": false + }, + "overdueReasons": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "index": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "available" + }, + "reason": { + "type": "string" + }, + "observedWatches": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "latestObservationAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestObservationAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "oldestObservationAgeSeconds": { + "anyOf": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "null" + } + ] + }, + "denominator": { + "type": "string" + } + }, + "required": [ + "status", + "reason", + "observedWatches", + "latestObservationAt", + "oldestObservationAt", + "oldestObservationAgeSeconds", + "denominator" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "denied" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "unavailable" + }, + "reason": { + "type": "string" + } + }, + "required": [ + "status", + "reason" + ], + "additionalProperties": false + } + ] + }, + "serviceTicks": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "available", + "unavailable" + ] + }, + "meaning": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "plan": { + "type": "string" + }, + "latestAttemptAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + "latestCompletionAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "latestSuccessAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "latestOutcome": { + "type": "string", + "enum": [ + "running", + "succeeded", + "failed" + ] + }, + "ageSeconds": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "plan", + "latestAttemptAt", + "latestCompletionAt", + "latestSuccessAt", + "latestOutcome", + "ageSeconds" + ], + "additionalProperties": false + } + } + }, + "required": [ + "status", + "meaning", + "items" + ], + "additionalProperties": false + }, + "assurance": { + "type": "object", + "properties": { + "elapsedSoak": { + "type": "string", + "const": "not_established" + }, + "backupRestore": { + "type": "string", + "const": "not_established" + }, + "externalDelivery": { + "type": "string", + "const": "not_established" + } + }, + "required": [ + "elapsedSoak", + "backupRestore", + "externalDelivery" + ], + "additionalProperties": false + }, + "denominator": { + "type": "string" + } + }, + "required": [ + "schemaVersion", + "workspaceId", + "projectId", + "asOf", + "source", + "target", + "sourceLinkVerification", + "sourceJobs", + "overdueReasons", + "index", + "serviceTicks", + "assurance", + "denominator" + ], + "additionalProperties": {} + } + }, { "name": "list_index_observations", "toolset": "evidence", @@ -21609,6 +25837,11 @@ export const CATALOG_COMMANDS = [ "type": "integer", "minimum": 1, "maximum": 100 + }, + "beforeId": { + "description": "Identifier of the before returned by its create or list operation.", + "type": "string", + "pattern": "^[a-f0-9]{64}$" } }, "required": [ @@ -21687,6 +25920,20 @@ export const CATALOG_COMMANDS = [ ], "additionalProperties": {} } + }, + "nextCursor": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean" } }, "required": [ diff --git a/cli/chronology.js b/cli/chronology.js new file mode 100644 index 0000000..fc438e0 --- /dev/null +++ b/cli/chronology.js @@ -0,0 +1,39 @@ +import {readFile,writeFile,mkdir,copyFile,unlink} from 'node:fs/promises'; +import {constants} from 'node:fs'; +import {dirname,resolve} from 'node:path'; +import {loadConfig,ConfigError} from './config.js'; +import {cloudConnection} from './connection.js'; +import {createClient} from './client.js'; +import {freezeChronology,normalizeLocalSubmissions} from '../shared/monitoring-chronology-export.js'; +const help='agentlinkops chronology pull --watch-id ID [--limit 50] [--cursor TOKEN] [--all] [--out NEW_FILE] | export --pages FILE [--submissions FILE] [--out NEW_FILE]'; +async function output(value,args,{cwd,out}){ + const text=JSON.stringify(value,null,2)+'\n'; + if(!args.out){out(text.trimEnd());return;} + if(typeof args.out!=='string'||!args.out.trim())throw new ConfigError('--out requires a file'); + const path=resolve(cwd,args.out),temp=`${path}.${crypto.randomUUID()}.tmp`; + await mkdir(dirname(path),{recursive:true}); + try{await writeFile(temp,text,{flag:'wx',mode:0o600});await copyFile(temp,path,constants.COPYFILE_EXCL);}finally{await unlink(temp).catch(e=>{if(e.code!=='ENOENT')throw e;});} + out(`wrote ${path}`); +} +export async function chronologyMain(args,{cwd=process.cwd(),env=process.env,fetchImpl=globalThis.fetch,out=console.log}={}){ + const command=args._[1],allowed=new Set(['_','tag','watch-id','limit','cursor','all','out','pages','submissions']); + if(args._.length!==2||args.tag?.length||Object.keys(args).some(key=>!allowed.has(key)))throw new ConfigError(help); + if(command==='export'){ + if(typeof args.pages!=='string'||args['watch-id']!==undefined||args.limit!==undefined||args.cursor!==undefined||args.all!==undefined)throw new ConfigError(help); + const input=JSON.parse(await readFile(resolve(cwd,args.pages),'utf8')),pages=Array.isArray(input)?input:[input]; + const localItems=args.submissions?await normalizeLocalSubmissions(JSON.parse(await readFile(resolve(cwd,args.submissions),'utf8')),pages[0]?.identity):[]; + await output(await freezeChronology(pages,{localItems}),args,{cwd,out});return 0; + } + if(command!=='pull'||typeof args['watch-id']!=='string'||args.pages!==undefined||args.submissions!==undefined||args.all!==undefined&&args.all!==true)throw new ConfigError(help); + const limit=Number(args.limit??50);if(!Number.isInteger(limit)||limit<1||limit>100||args.cursor===true)throw new ConfigError(help); + const config=await loadConfig({cwd}),connection=cloudConnection(config,env); + if(!connection.projectId)throw new ConfigError('Connect this project before reading hosted chronology.'); + const client=createClient({...connection,fetchImpl}),pages=[];let cursor=args.cursor; + do{ + const page=await client.callCommand('get_placement_chronology',{watchId:args['watch-id'],limit,...(cursor?{cursor}: {})}); + if(page.identity?.projectId!==connection.projectId||page.identity?.watchId!==args['watch-id']||connection.workspaceId&&page.identity?.workspaceId!==connection.workspaceId)throw new ConfigError('Chronology identity does not match the connected project and selected watch.'); + pages.push(page);cursor=page.nextCursor; + if(pages.length===100&&args.all&&cursor)throw new ConfigError('Chronology exceeds the 100-page read bound. Pull explicit pages and export their qualified partial dataset.'); + }while(args.all&&cursor); + await output(args.all?await freezeChronology(pages):pages[0],args,{cwd,out});return 0; +} diff --git a/cli/client.js b/cli/client.js index a631b6b..b49acca 100644 --- a/cli/client.js +++ b/cli/client.js @@ -5,7 +5,7 @@ export {CloudError} from './cloud-error.js'; // A small HTTP client for the cloud. Scoped keys only; no human session ever reaches here. // DP-0036-T13: the CLI names itself so the usage counters can tell it from other REST callers. // The version follows the plugin manifests; bump it with them. -export const CLI_VERSION = '0.6.12'; +export const CLI_VERSION = '0.6.13'; export const CLI_USER_AGENT = `agentlinkops-cli/${CLI_VERSION}`; const MAX_ATTEMPTS = 3; const MAX_RETRY_WAIT_MS = 5_000; diff --git a/cli/fleet.js b/cli/fleet.js index 81ad3c1..d1cce9b 100644 --- a/cli/fleet.js +++ b/cli/fleet.js @@ -6,54 +6,62 @@ // and a summary that deduplicated them would silently pick one. So every number below is // per project, the only cross-project arithmetic is a sum of counts, and the output says so. // -// "Earned" follows the ledger contract, not a status column: an entry is earned when its -// latest OBSERVATION says present. A project with no observation file has earned nothing and -// is reported as never checked rather than as zero links. +// "Earned" follows known conclusive presence; a later inconclusive attempt is disclosed +// separately. Activity is supplied explicitly per project, never discovered in another repo. import { readLedger } from './ledger.js'; -import { readObservations, latestByEntry } from './mirror.js'; +import { readObservations } from './mirror.js'; +import { readState } from './state.js'; +import { projectPlacements } from './placement-projection.js'; const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`; /** One project's row. Everything is computed inside the project; nothing crosses the boundary. */ -export async function fleetProject(name, ledgerPath, observationsPath) { +export async function fleetProject(name, ledgerPath, observationsPath, statePath = null) { const ledger = await readLedger(ledgerPath); if (ledger.missing) { const error = new Error(`no ledger at ${ledgerPath}`); error.code = 'NO_LEDGER'; throw error; } const byIntent = { wanted: 0, expected: 0, retired: 0 }; for (const entry of ledger.entries) byIntent[entry.intent] = (byIntent[entry.intent] ?? 0) + 1; const { rows } = observationsPath ? await readObservations(observationsPath) : { rows: [] }; - const latest = latestByEntry(rows); - let earned = 0, observed = 0; - for (const [id, row] of latest) { - if (!ledger.entries.some(entry => entry.id === id)) continue; // observations from another ledger's ids - observed++; - if (row.state === 'present') earned++; - } - const lastCheck = rows.length ? rows.map(row => row.checked_at).sort().at(-1) : null; + const state = statePath ? await readState(statePath) : null; + const eligible = ledger.entries.filter(entry => entry.intent !== 'retired'); + const projected = projectPlacements(eligible, rows, { state }); + const observed = projected.filter(item => item.latest_attempt).length; + const earned = projected.filter(item => item.current_state === 'present').length; + const newest = values => values.filter(Boolean).sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null; return { project: name, ledger: ledgerPath, entries: ledger.entries.length, ...byIntent, problems: ledger.problems.length, observed_entries: observed, earned, - never_checked: ledger.entries.filter(entry => entry.intent !== 'retired').length - observed, - last_observation: lastCheck, + never_checked: eligible.length - observed, + last_observation: newest(projected.map(item => item.evidence_observed_at)), + last_attempt_at: newest(projected.map(item => item.latest_attempt?.checked_at)), + last_success_at: newest(projected.map(item => item.last_successful_observation?.checked_at)), + last_link_verification_at: newest(projected.map(item => item.last_link_verification?.checked_at)), + uncertain_entries: projected.filter(item => item.uncertain).length, + activity_available: projected.some(item => item.activity_available), + activity_entries: projected.filter(item => item.activity_available).length, + coverage: { denominator: 'non_retired_ledger_entries', eligible_entries: eligible.length, + identity_unmatched_entries: projected.filter(item => item.identity_status === 'not_matched').length, + activity_input: statePath ? 'explicit_state_file' : 'not_supplied' }, // Due volume, the same arithmetic the dogfood records use to price a fleet. implied_weekly_checks: byIntent.wanted + byIntent.expected * 7, }; } /** Summons the fleet. Same numbers per project, one sum of counts, no merged intent. */ -export async function fleetSummary(projects, { observationsOf = {} } = {}) { +export async function fleetSummary(projects, { observationsOf = {}, statesOf = {} } = {}) { const rows = []; - for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null)); + for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null, statesOf[name] ?? null)); const totals = rows.reduce((acc, row) => { - for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'implied_weekly_checks']) { + for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'uncertain_entries', 'activity_entries', 'implied_weekly_checks']) { acc[key] = (acc[key] ?? 0) + row[key]; } return acc; }, {}); return { - v: 1, projects: rows, totals, + v: 2, projects: rows, totals, // The sum answers "how big is the operation". It is not a merged view: the same platform // counted in two projects is two intents, and only a human can say which one they meant. intent_merged_across_repos: false, @@ -75,6 +83,9 @@ export function renderFleet(summary, out = console.log) { + `${String(t.never_checked).padStart(13)} (sum of counts)`); out(''); out(`implied weekly check volume across the fleet: ${t.implied_weekly_checks}`); + for (const row of summary.projects) { + out(`${row.project}: latest attempt ${row.last_attempt_at ?? 'not recorded'}; last conclusive check ${row.last_success_at ?? 'not recorded'}; last link verification ${row.last_link_verification_at ?? 'not recorded'}; activity ${row.activity_available ? `${row.activity_entries} entr(ies)` : 'not available'}; uncertain ${row.uncertain_entries}`); + } if (summary.projects.some(row => row.problems)) { out(`ledger problems: ${summary.projects.map(row => `${row.project}=${row.problems}`).filter(s => !s.endsWith('=0')).join(', ')}`); } diff --git a/cli/main.js b/cli/main.js index 42f21b8..ab0aa3b 100644 --- a/cli/main.js +++ b/cli/main.js @@ -1,3 +1,5 @@ +import {projectPlacements} from './placement-projection.js'; +import {chronologyMain} from './chronology.js'; import { acquireSyncLock } from './sync-lock.js'; import {lifecycleMain} from './lifecycle.js'; import {admissionMain} from './admission.js'; @@ -52,6 +54,8 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository [--mode local|hosted|external] read-only setup plan; no account required agentlinkops add --source URL --target URL [--intent wanted|expected] [--scope …] [--anchor TEXT] [--rel a,b] [--ref TEXT] [--tag t --tag t] [--note TEXT] + agentlinkops chronology pull --watch-id ID [--limit N] [--cursor TOKEN] [--all] [--out FILE] + agentlinkops chronology export --pages FILE [--submissions FILE] [--out FILE] agentlinkops index pull --watch-id ID [--limit N] [--before ID] agentlinkops lifecycle get|update|clear|renew|report agentlinkops admission list|add|update|remove|evaluate|reevaluate|receipt --project ID @@ -63,7 +67,7 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository agentlinkops import FILE --target DOMAIN [--from SUPPLIER] [--map source=COL,target=COL] [--exact-url] [--no-subdomains] [--generated-at ISO] [--json] agentlinkops adopt CRM.sqlite [--scope exact|domain] [--write] - agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--json] + agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--state-of NAME=FILE] [--json] agentlinkops platforms [HOST] [--observations FILE]… [--benchmark FILE]… [--seed FILE]… [--for-candidates FILE] [--json] agentlinkops context COMMAND… first-party search context and site profile @@ -100,7 +104,7 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository agentlinkops adopt-result FILE [--intent wanted|expected] save result to local ledger agentlinkops receipt add --file CLAIM.json project.site declares public site hosts agentlinkops receipt history [--performance FILE.json] claims, history and optional dated context - agentlinkops status [--json] + agentlinkops status [--all] [--json] agentlinkops diff [--json] agentlinkops report [--out FILE] [--title T] [--brand B] [--as-of ISO] [--include-notes] [--include-retired] [--digest-only] [--json] @@ -122,7 +126,7 @@ evidence · 2 usage, configuration or ledger error. Unknown is nonfailing by def Names: \`linktrail\` still runs this CLI, LINKTRAIL_* variables are still read, and an existing .linktrail/ directory is still used, each with a one-line notice, until the DP-0029 cutover.`; -const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "observations", "benchmark", "seed"],{tag:[]}); +const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "state-of", "observations", "benchmark", "seed"],{tag:[]}); const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`; @@ -154,6 +158,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), if (command === 'lifecycle') return await lifecycleMain(args,{cwd,env,fetchImpl,out,err}); if (command === 'admission') { const config=await loadConfig({cwd}); return await admissionMain(argv.slice(1),{client:createClient({...cloudConnection(config,env),fetchImpl}),out,err}); } if (command === 'disavow') return await disavowMain(args,{cwd,env,fetchImpl,out}); + if (command === 'chronology') return await chronologyMain(args,{cwd,env,fetchImpl,out}); if (command === 'index') return await indexMain(args,{cwd,env,fetchImpl,out}); if (command === 'setup') return await setupPlanMain(argv, { cwd, out }); if (command === 'tools' || command === 'describe' || command === 'schema' || command === 'call') return await commandsMain(argv, { cwd, out, err, env, fetchImpl }); @@ -306,7 +311,15 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), if (!name || !path) throw new ConfigError(`--observations-of expects NAME=FILE, got "${value}"`); observationsOf[name] = path; } - const summary = await fleetSummary(pairs, { observationsOf }); + const statesOf = {}; + for (const value of [].concat(args['state-of'] ?? [])) { + const separator = String(value).indexOf('='); + const name = String(value).slice(0, separator), path = String(value).slice(separator + 1); + if (separator < 1 || !path || !pairs.some(project => project.name === name)) throw new ConfigError('--state-of expects a declared NAME=FILE'); + if (Object.hasOwn(statesOf, name)) throw new ConfigError(`Duplicate state input for ${name}`); + statesOf[name] = path; + } + const summary = await fleetSummary(pairs, { observationsOf, statesOf }); if (args.json) { out(JSON.stringify(summary, null, 1)); return 0; } return renderFleet(summary, out); } @@ -392,6 +405,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), // so it is unchanged by a cosmetic edit to the template and changes the moment a figure does. const asOf = args['as-of'] === true ? null : (args['as-of'] ?? null); const dataset = freezeDataset(ledger.entries, observations.rows, { + activityState: await readState(config.paths.state), asOf: asOf ?? new Date().toISOString().slice(0, 10), title: args.title === true ? 'Backlink report' : (args.title ?? 'Backlink report'), includeNotes: args['include-notes'] === true, @@ -500,24 +514,29 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), } if (command === 'status' || command === 'diff') { - const found = command === 'status' ? disagreements(ledger.entries, observations.rows) : transitions(observations.rows); - if (args.json) { out(JSON.stringify(found, null, 1)); return 0; } + const activityState = command === 'status' ? await readState(config.paths.state) : null; + const found = command === 'status' ? disagreements(ledger.entries, observations.rows, { state: activityState }) : transitions(observations.rows); + const activity = command === 'status' && args.all ? projectPlacements(ledger.entries, observations.rows, { state: activityState }).map(({ entry, row, ...facts }) => ({ id: entry.id, source: entry.source, target: entry.target, scope: entry.scope, ...facts })) : null; + if (args.json) { out(JSON.stringify(activity ? { disagreements: found, placements: activity } : found, null, 1)); return 0; } reportProblems(ledger.problems, 'ledger line(s)', out); if (command === 'diff') { if (!found.length) out('no state changes recorded'); for (const change of found.slice(0, 100)) out(` ${change.at.slice(0, 10)} ${change.from} -> ${change.to} ${change.id}`); return 0; } - const groups = { appeared: 'appeared', lost: 'LOST', suspected_missing: 'suspected missing', cannot_say: 'cannot say', never_checked: 'never checked' }; + const groups = { appeared: 'appeared', lost: 'confirmed missing', suspected_missing: 'suspected missing', cannot_say: 'cannot say', never_checked: 'never checked' }; for (const [kind, label] of Object.entries(groups)) { const rows = found.filter(item => item.kind === kind); if (!rows.length) continue; out(`\n${label} (${rows.length})`); for (const item of rows.slice(0, 50)) { out(` ${item.entry.id} ${item.entry.source}${item.row ? ` [${item.row.reason}]` : ''}`); + out(` Latest attempt: ${item.activity?.latest_attempt?.checked_at ?? 'not retained'} (${item.activity?.latest_attempt?.reason ?? 'unknown'})`); + out(` Last conclusive: ${item.activity?.last_successful_observation?.checked_at ?? 'not retained'}; last link verification: ${item.activity?.last_link_verification?.checked_at ?? 'not retained'}`); } } if (!found.length) out('intent and observations agree on every entry'); + if (activity) for (const item of activity) out(` ${item.id}: latest attempt ${item.latest_attempt?.checked_at ?? 'not retained'}; last conclusive ${item.last_successful_observation?.checked_at ?? 'not retained'}; last link verification ${item.last_link_verification?.checked_at ?? 'not retained'}`); return 0; } @@ -590,7 +609,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(), if (ledger.problems.length) { reportProblems(ledger.problems, 'ledger line(s)', err); return 2; } const state = await readState(config.paths.state); const selected = selectEntries(ledger.entries, { filter: args.filter === true ? null : args.filter }); - const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state)); + const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state, selected)); if (!entries.length) { (args.json ? err : out)(`nothing due (${plural(selected.length, 'entry', 'entries')} in scope; pass --all to recheck)`); return 0; } if (!args.json) err(`checking ${plural(entries.length, 'entry', 'entries')}…`); const rows = await runCheck(entries, { diff --git a/cli/mcp-client.js b/cli/mcp-client.js index fac0922..0935424 100644 --- a/cli/mcp-client.js +++ b/cli/mcp-client.js @@ -62,7 +62,7 @@ function retryDelay(error, attempt, now) { async function sdkClient({url, token, workspaceId, fetchImpl}) { const {Client, StreamableHTTPClientTransport} = await import('@modelcontextprotocol/client'); - const client = new Client({name: 'agentlinkops-cli', version: '0.6.12'}); + const client = new Client({name: 'agentlinkops-cli', version: '0.6.13'}); const destination = new URL(url); const transport = new StreamableHTTPClientTransport(destination, { authProvider: {token: async () => token}, @@ -71,7 +71,7 @@ async function sdkClient({url, token, workspaceId, fetchImpl}) { if (target.href !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH'); const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined)); if (workspaceId) headers.set('X-Workspace-ID', workspaceId); - headers.set('User-Agent', 'agentlinkops-cli/0.6.12'); + headers.set('User-Agent', 'agentlinkops-cli/0.6.13'); const response = await fetchImpl(input, {...init, headers, redirect: 'error', signal: AbortSignal.timeout(30000)}); if (response.redirected || response.status >= 300 && response.status < 400 || response.url && response.url !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH'); diff --git a/cli/placement-projection.js b/cli/placement-projection.js new file mode 100644 index 0000000..ec7d8ca --- /dev/null +++ b/cli/placement-projection.js @@ -0,0 +1,106 @@ +// One identity-bound projection for local activity and retained observations. A change-only +// mirror is not a complete probe log, so local absence remains provisional. Hosted watch +// state is carried independently from the raw check; this module never confirms loss. +import { mirrorEvidenceReference } from '../src/evidence-reference.js'; + +const conclusive = new Set(['present', 'absent', 'source_unavailable']); +const watchStates = new Set(['present', 'suspected_missing', 'confirmed_missing', 'source_unavailable']); +export const validTime = value => typeof value === 'string' && Number.isFinite(Date.parse(value)); + +export function placementIdentity(value) { + const record = value?.result ?? value; + const source = record?.sourceUrl ?? record?.source_url ?? record?.source; + const target = record?.targetUrl ?? record?.target_url ?? record?.target; + const scope = record?.targetScope ?? record?.target_scope ?? record?.scope ?? 'exact'; + return typeof source === 'string' && typeof target === 'string' + ? JSON.stringify([source, target, scope]) : null; +} + +export function attemptSummary(row, { retained = false } = {}) { + if (!validTime(row?.checked_at)) return null; + const cloud = row.source === 'cloud'; + const rawState = cloud && conclusive.has(row.result?.checkState) ? row.result.checkState + : ['suspected_missing', 'confirmed_missing'].includes(row.state) ? 'absent' : row.state; + const method = row.result?.evidence?.method ?? null; + return { + checked_at: row.checked_at, placement: placementIdentity(row), + state: rawState, reason: row.reason ?? null, complete: row.complete === true, + origin: row.source ?? 'local', + watch_state: cloud && watchStates.has(row.result?.watchState) ? row.result.watchState + : ['suspected_missing', 'confirmed_missing'].includes(row.state) ? row.state : null, + link_signature: row.result?.linkSignature ?? null, + method, checker_version: row.checker_version ?? row.result?.evidence?.checkerVersion ?? null, + content_hash: row.result?.evidence?.sha256 ?? null, + evidence_reference: retained ? mirrorEvidenceReference({ entryId: row.id, checkedAt: row.checked_at, origin: row.source ?? 'local' }) : null, + retained, + }; +} + +export const isConclusive = attempt => attempt?.complete === true && conclusive.has(attempt.state); +export const isLinkVerification = attempt => isConclusive(attempt) && ['present', 'absent'].includes(attempt.state); + +// Equal timestamps never qualify another probe. Stable tie-breaking makes frozen exports +// independent of input ordering; an activity summary cannot replace a richer retained receipt. +const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0; +function latest(items) { + return items.filter(item => item && validTime(item.checked_at)).sort((a, b) => + Date.parse(b.checked_at) - Date.parse(a.checked_at) + || Number(b.retained === true) - Number(a.retained === true) + || compareText(JSON.stringify(a), JSON.stringify(b)))[0] ?? null; +} + +function activityAttempt(record, identity) { + if (record?.last_placement !== identity) return null; + if (record.last_attempt?.placement === identity && record.last_attempt.checked_at === record.last_checked + && validTime(record.last_attempt.checked_at)) return record.last_attempt; + // Legacy activity can prove its last conclusive time, but cannot invent a method/hash or + // reconstruct the overwritten successful check after a later unknown attempt. + if (!validTime(record.last_checked)) return null; + return { checked_at: record.last_checked, placement: identity, state: record.last_state ?? 'unknown', + reason: record.last_reason ?? null, complete: record.last_complete === true, origin: 'local', + watch_state: null, link_signature: record.last_signature ?? null, method: null, + checker_version: null, content_hash: null, evidence_reference: null, retained: false }; +} + +export function projectPlacement(entry, observations, { state = null } = {}) { + const identity = placementIdentity(entry); + const history = identity ? observations.filter(row => row.id === entry.id && placementIdentity(row) === identity && validTime(row.checked_at)) : []; + const attempts = history.map(row => attemptSummary(row, { retained: true })); + const record = state?.entries?.[entry.id]; + const locate = item => item?.retained === true && !attempts.some(receipt => receipt.checked_at === item.checked_at + && receipt.origin === item.origin && receipt.content_hash === item.content_hash) + ? { ...item, retained: false, evidence_reference: null } : item; + const activity = locate(identity ? activityAttempt(record, identity) : null); + const saved = key => locate(identity && record?.last_placement === identity && record[key]?.placement === identity ? record[key] : null); + const latestAttempt = latest([...attempts, activity]); + const savedSuccess = saved('last_successful_observation'), savedLink = saved('last_link_verification'); + const lastSuccessful = latest([...attempts.filter(isConclusive), isConclusive(savedSuccess) ? savedSuccess : null, isConclusive(activity) ? activity : null]); + const lastLink = latest([...attempts.filter(isLinkVerification), isLinkVerification(savedLink) ? savedLink : null, isLinkVerification(activity) ? activity : null]); + const retained = latest(attempts); + const row = retained ? history.filter(item => JSON.stringify(attemptSummary(item, { retained: true })) === JSON.stringify(retained)) + .sort((a, b) => compareText(JSON.stringify(a), JSON.stringify(b)))[0] ?? null : null; + const firstDates = attempts.filter(item => isConclusive(item) && item.state === 'present').map(item => item.checked_at); + const first = saved('first_present_observation'); + if (isConclusive(first) && first.state === 'present' && validTime(first.checked_at)) firstDates.push(first.checked_at); + const firstPresent = firstDates.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? null; + const current = latestAttempt?.watch_state ?? lastSuccessful?.watch_state ?? (lastSuccessful?.state === 'absent' ? 'suspected_missing' + : lastSuccessful?.state ?? (latestAttempt ? 'unknown' : 'unchecked')); + return { + entry, row, placement: identity, current_state: current, + uncertain: !!latestAttempt && !isConclusive(latestAttempt), + latest_attempt: latestAttempt, last_successful_observation: lastSuccessful, + last_link_verification: lastLink, first_present: firstPresent, + evidence_observed_at: retained?.checked_at ?? null, + activity_available: activity !== null, + identity_status: history.length || activity ? 'matched' : observations.some(item => item.id === entry.id) || record ? 'not_matched' : 'not_observed', + }; +} + +export function projectPlacements(entries, observations, options = {}) { + const byId = new Map(); + for (const row of observations) { + if (!byId.has(row.id)) byId.set(row.id, []); + byId.get(row.id).push(row); + } + return entries.map(entry => projectPlacement(entry, byId.get(entry.id) ?? [], options)); +} diff --git a/cli/report.js b/cli/report.js index 896cced..d341714 100644 --- a/cli/report.js +++ b/cli/report.js @@ -18,13 +18,15 @@ // evidence reference that resolves to the observation itself. One dataset feeds both exports: // this HTML view for a human client, and `report --json` for an agent, so the two can never // disagree about what was observed. +// v3 freezes activity separately from retained receipts. Older v2 datasets still render with +// their original columns and wording; activity never changes an old observation's date/hash. import {indexRowsForUrl} from './index-observations.js'; import {reconcileIndexObservations} from '../src/index-observation.js'; import { createHash } from 'node:crypto'; -import { latestByEntry } from './mirror.js'; +import { projectPlacements } from './placement-projection.js'; import { mirrorEvidenceReference } from '../src/evidence-reference.js'; -export const REPORT_VERSION = 2; +export const REPORT_VERSION = 3; /** HTML-escape. Every value below is publisher or customer content and none of it is trusted. */ export const escape = value => String(value ?? '') @@ -35,6 +37,10 @@ const STATE_LABEL = Object.freeze({ present: 'Present', absent: 'Not found', unknown: 'Could not check', source_unavailable: 'Page unavailable', unchecked: 'Never checked', }); +const CURRENT_STATE_LABEL = Object.freeze({ + ...STATE_LABEL, + suspected_missing: 'Suspected missing', confirmed_missing: 'Confirmed missing', +}); /** * The frozen dataset a report renders. @@ -42,12 +48,10 @@ const STATE_LABEL = Object.freeze({ * Selection happens here and nowhere else, so a caller can hold this object, assert it, and know * the rendered document is a pure function of it. */ -export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false, indexObservations = [] } = {}) { - const latest = latestByEntry(observations); - const rows = entries - .filter(entry => includeRetired || entry.intent !== 'retired') - .map(entry => { - const row = latest.get(entry.id) ?? null; +export function freezeDataset(entries, observations, { asOf, title = 'Backlink report', includeNotes = false, includeRetired = false, indexObservations = [], activityState = null } = {}) { + const rows = projectPlacements(entries.filter(entry => includeRetired || entry.intent !== 'retired'), observations, { state: activityState }) + .map(projection => { + const { entry, row } = projection; const indexRows=indexRowsForUrl(indexObservations,entry.source); const origin = row?.source === 'cloud' ? 'cloud' : (row?.source ?? null); return { @@ -57,6 +61,16 @@ export function freezeDataset(entries, observations, { asOf, title = 'Backlink r reason: row?.reason ?? null, occurrences: row?.occurrences ?? 0, checked_at: row?.checked_at ?? null, + current_state: projection.current_state, + uncertain: projection.uncertain, + activity: { + available: projection.activity_available, + identity_status: projection.identity_status, + latest_attempt: projection.latest_attempt, + last_successful_observation: projection.last_successful_observation, + last_link_verification: projection.last_link_verification, + first_present: projection.first_present, + }, checked_by: row?.source ?? null, anchor: row?.result?.occurrences?.[0]?.anchor ?? null, rel: row?.result?.occurrences?.[0]?.rel ?? [], @@ -105,10 +119,16 @@ export function freezeDataset(entries, observations, { asOf, title = 'Backlink r // Stated rather than implied. A report over a ledger nobody has checked is not a report about // links, and the reader should see that before the table. coverage: { - checked: rows.filter(row => row.checked_at).length, - never_checked: rows.filter(row => !row.checked_at).length, + checked: rows.filter(row => row.activity.latest_attempt).length, + never_checked: rows.filter(row => !row.activity.latest_attempt).length, + retained_receipt_entries: rows.filter(row => row.checked_at).length, + missing_receipt_entries: rows.filter(row => !row.checked_at).length, observed_from: dates[0] ?? null, observed_to: dates.at(-1) ?? null, + activity_entries: rows.filter(row => row.activity.available).length, + identity_unmatched_entries: rows.filter(row => row.activity.identity_status === 'not_matched').length, + denominator: includeRetired ? 'all_ledger_entries' : 'non_retired_ledger_entries', + known_present_after_unknown: rows.filter(row => row.current_state === 'present' && row.uncertain).length, }, }; } @@ -149,6 +169,18 @@ export function renderReport(dataset, { brand = null } = {}) { const digest = createHash('sha256').update(JSON.stringify(dataset)).digest('hex'); const card = (label, value) => `
${escape(value)}${escape(label)}
`; const coverage = dataset.coverage; + const hasActivity = dataset.v >= 3; + const activityCell = row => { + const activity = row.activity ?? {}; + const describe = (label, attempt) => `
${escape(label)}: ${attempt ? `${escape(attempt.state)} · ${escape(attempt.checked_at)}` : 'not retained'}
` + + (attempt ? `
${escape(attempt.reason ?? 'reason not recorded')} · ${escape(attempt.method ?? 'method not recorded')}
` + + `
${escape(attempt.evidence_reference ?? 'no retained receipt reference')}
` : ''); + return `
${escape(CURRENT_STATE_LABEL[row.current_state] ?? row.current_state ?? 'Unknown')}${row.uncertain ? ' · latest attempt inconclusive' : ''}
` + + describe('Latest attempt', activity.latest_attempt) + + describe('Last conclusive check', activity.last_successful_observation) + + describe('Last link verification', activity.last_link_verification) + + `
Activity ${activity.available ? 'recorded' : 'not supplied or not retained'}; identity ${escape(activity.identity_status ?? 'not retained')}
`; + }; // The receipt column. "hash not recorded" is stated rather than papered over: a local check // that never completed a fetch has no hash, and a cloud-synced row may carry its snapshot key // without a hash in an older event. Neither is a claim about the link. @@ -164,10 +196,10 @@ export function renderReport(dataset, { brand = null } = {}) { const rows = dataset.rows.map(row => ` ${escape(row.source)} ${escape(row.target)} - ${escape(STATE_LABEL[row.state] ?? row.state)} + ${escape(hasActivity && !row.evidence && row.activity?.latest_attempt ? 'No retained observation' : (hasActivity ? CURRENT_STATE_LABEL : STATE_LABEL)[row.state] ?? row.state)} ${row.anchor ? escape(row.anchor) : '—'}${(row.rel ?? []).length ? `
rel: ${escape(row.rel.join(' '))}
` : ''} ${row.checked_at ? escape(row.checked_at.slice(0, 10)) : 'never'} - ${evidenceCell(row)} + ${evidenceCell(row)}${hasActivity ? `\n ${activityCell(row)}` : ''} ${hasIndex?`${(row.index_observations??[]).map(o=>`${escape(o.tier)} · ${escape(o.reason)}
${escape(o.checked_at)} · ${escape(o.backend)}
${escape(o.locator)}
`).join('
')||'Not checked'}`:''} ${dataset.include_notes ? `${escape(row.ref ?? '')}${row.note ? `
${escape(row.note)}
` : ''}` : ''} `).join('\n'); @@ -188,21 +220,21 @@ export function renderReport(dataset, { brand = null } = {}) {

${brand ? `${escape(brand)} · ` : ''}Prepared ${escape(dataset.as_of)}

${card('links in report', dataset.totals.entries)} -${card('present', dataset.totals.present ?? 0)} -${card('not found', dataset.totals.absent ?? 0)} -${card('could not check', (dataset.totals.unknown ?? 0) + (dataset.totals.source_unavailable ?? 0))} -${card('never checked', dataset.totals.unchecked ?? 0)} +${card(hasActivity ? 'retained present' : 'present', dataset.totals.present ?? 0)} +${card(hasActivity ? 'retained not found' : 'not found', dataset.totals.absent ?? 0)} +${card(hasActivity ? 'retained could not check' : 'could not check', (dataset.totals.unknown ?? 0) + (dataset.totals.source_unavailable ?? 0))} +${card('never checked', hasActivity ? coverage.never_checked : dataset.totals.unchecked ?? 0)}

What these dates mean. Every row shows the date that link was last observed, between ${escape(coverage.observed_from ?? 'n/a')} and ${escape(coverage.observed_to ?? 'n/a')}. The date at the top is when this document was prepared and does not make any observation newer -than it is. ${coverage.never_checked ? `${coverage.never_checked} of ${dataset.totals.entries} links have never been checked and are listed as such.` : ''}

+than it is. ${coverage.never_checked ? `${coverage.never_checked} of ${dataset.totals.entries} links have never been checked and are listed as such.` : ''}

${hasActivity ? '\n

Activity and verification. 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.

' : ''}

What a result means. “Present” means the link was in the observed HTML. “Could not check” is not the same as “not found”: it means the page could not be read, and no conclusion was drawn. Each receipt identifies the check method when it was recorded. A snapshot alone does not prove the link was visible on screen.

-${hasIndex?'':''}${dataset.include_notes ? '' : ''} +${hasActivity ? '' : ''}${hasIndex?'':''}${dataset.include_notes ? '' : ''} ${rows}
Source pageTargetResultAnchorObservedEvidenceIndex evidenceNotesSource pageTargetResultAnchorObservedEvidenceActivity and verified stateIndex evidenceNotes
@@ -219,7 +251,7 @@ ${receiptRows} ` : ''} diff --git a/cli/state.js b/cli/state.js index e3e6098..c71de1d 100644 --- a/cli/state.js +++ b/cli/state.js @@ -10,8 +10,9 @@ // was this last confirmed" — is exactly what a small, rewritten map answers better. import { readFile, writeFile, mkdir, rename } from 'node:fs/promises'; import { dirname, join } from 'node:path'; +import { placementIdentity, attemptSummary, isConclusive, isLinkVerification, validTime } from './placement-projection.js'; -export const STATE_VERSION = 2; +export const STATE_VERSION = 3; /** * Brings a state file forward without losing anything it already held. @@ -58,49 +59,81 @@ export async function writeState(path, state) { } /** When each entry was last checked, whatever the answer was. */ -export function lastCheckedMap(state) { - return new Map(Object.entries(state.entries ?? {}).map(([id, record]) => [id, record.last_checked])); +export function lastCheckedMap(state, entries = null) { + const identities = entries ? new Map(entries.map(entry => [entry.id, placementIdentity(entry)])) : null; + return new Map(Object.entries(state.entries ?? {}).filter(([id, record]) => !identities + || identities.get(id) && record.last_placement === identities.get(id)).map(([id, record]) => [id, record.last_checked])); +} + +const methodOf = row => row.result?.evidence?.method ?? null; +const eligible = (row, previous) => validTime(row.checked_at) + && (!validTime(previous?.last_checked) || Date.parse(row.checked_at) > Date.parse(previous.last_checked)); +const repeats = (row, previous) => previous + && previous.last_placement === placementIdentity(row) + && previous.last_state === row.state + && (previous.last_reason ?? null) === (row.reason ?? null) + && (previous.last_signature ?? null) === (row.result?.linkSignature ?? null) + && previous.last_complete === (row.complete === true) + && (previous.last_method ?? null) === methodOf(row) + && (previous.last_checker_version ?? null) === (row.checker_version ?? row.result?.evidence?.checkerVersion ?? null); + +function applyRow(previous, row) { + const identity = placementIdentity(row); + const samePlacement = previous?.last_placement === identity; + const attempt = attemptSummary(row, { retained: !repeats(row, previous) }); + const knownPresent = isConclusive(attempt) && attempt.state === 'present'; + const priorFirst = samePlacement && previous?.first_present_observation?.placement === identity + && isConclusive(previous.first_present_observation) && previous.first_present_observation.state === 'present' + ? previous.first_present_observation : null; + const firstPresent = priorFirst ?? (knownPresent ? attempt : null); + return { + ...previous, + last_checked: row.checked_at, + last_complete: row.complete === true, + last_placement: identity, + last_state: row.state, + last_reason: row.reason ?? null, + last_signature: row.result?.linkSignature ?? null, + last_method: methodOf(row), + last_checker_version: row.checker_version ?? row.result?.evidence?.checkerVersion ?? null, + checks: (previous?.checks ?? 0) + 1, + last_attempt: attempt, + last_successful_observation: isConclusive(attempt) ? attempt : samePlacement ? previous?.last_successful_observation ?? null : null, + last_link_verification: isLinkVerification(attempt) ? attempt : samePlacement ? previous?.last_link_verification ?? null : null, + first_present: firstPresent?.checked_at ?? null, + first_present_placement: firstPresent ? identity : null, + first_present_observation: firstPresent, + ...(samePlacement && previous?.first_present && !previous.first_present_observation + ? { legacy_first_present: previous.first_present } : {}), + }; } /** * Splits a run's rows into the ones worth recording and the ones that repeat. * - * A row is worth recording when it is the first for its entry, or when the state, the reason or - * the link signature moved. The signature is in there deliberately: a link that is still present - * but whose anchor or rel changed is a real event, and comparing only the state would miss it. + * A row is worth recording when it is the first for its placement, or when the state, reason, + * link signature, completeness or method moved. A link that is still present but whose anchor + * or rel changed is a real event, and comparing only the state would miss it. */ export function selectChanged(rows, state) { - const changed = [], repeated = []; + const changed = [], repeated = [], ignored = []; + const entries = { ...(state.entries ?? {}) }; for (const row of rows) { - const previous = state.entries?.[row.id]; - const signature = row.result?.linkSignature ?? null; - const same = previous - && previous.last_state === row.state - && (previous.last_reason ?? null) === (row.reason ?? null) - && (previous.last_signature ?? null) === signature; - (same ? repeated : changed).push(row); + const previous = entries[row.id]; + if (!eligible(row, previous)) { ignored.push(row); continue; } + (repeats(row, previous) ? repeated : changed).push(row); + entries[row.id] = applyRow(previous, row); } - return { changed, repeated }; + return { changed, repeated, ignored }; } /** Applies a run to the activity record. Every check counts, recorded or not. */ export function applyRun(state, rows) { const entries = { ...(state.entries ?? {}) }; for (const row of rows) { - const previous = entries[row.id] ?? { checks: 0 }; - entries[row.id] = { - ...previous, - last_checked: row.checked_at, - last_complete: row.complete === true, - last_placement: row.result?.sourceUrl && row.result?.targetUrl ? JSON.stringify([row.result.sourceUrl, row.result.targetUrl, row.result.targetScope ?? 'exact']) : null, - last_state: row.state, - last_reason: row.reason ?? null, - last_signature: row.result?.linkSignature ?? null, - checks: (previous.checks ?? 0) + 1, - // The date a placement was first seen is worth keeping where nothing can compact it away. - first_present: previous.first_present ?? (row.state === 'present' ? row.checked_at : null), - 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, - }; + const previous = entries[row.id]; + if (!eligible(row, previous)) continue; + entries[row.id] = applyRow(previous, row); } return { ...state, v: STATE_VERSION, entries }; } diff --git a/cli/status.js b/cli/status.js index 83e7308..94ee5ea 100644 --- a/cli/status.js +++ b/cli/status.js @@ -4,32 +4,28 @@ // expectation, closes the laptop, and three weeks later an agent opens the repository and reads // this. So it answers in the order a person cares about, not the order the data is stored in: // what appeared, what was lost, what we cannot say, and what needs a decision. -import { latestByEntry } from './mirror.js'; +import { projectPlacements } from './placement-projection.js'; /** A ledger entry whose observations disagree with its declared intent. */ -export function disagreements(entries, observations) { - const latest = latestByEntry(observations); +export function disagreements(entries, observations, { state = null } = {}) { const out = []; - for (const entry of entries) { - const row = latest.get(entry.id); - if (!row) { out.push({ entry, kind: 'never_checked' }); continue; } + for (const projection of projectPlacements(entries, observations, { state })) { + const { entry, row, latest_attempt: attempt } = projection; + const activity = { current_state: projection.current_state, uncertain: projection.uncertain, + latest_attempt: attempt, last_successful_observation: projection.last_successful_observation, + last_link_verification: projection.last_link_verification, + evidence_observed_at: projection.evidence_observed_at, identity_status: projection.identity_status }; + if (!attempt) { out.push({ entry, kind: 'never_checked', activity }); continue; } if (entry.intent === 'expected') { - const hostedState = row.source === 'cloud' ? row.result?.watchState : null; - const confirmed = row.state === 'confirmed_missing' - || (row.state === 'absent' && hostedState === 'confirmed_missing'); - const suspected = row.state === 'suspected_missing' || row.state === 'absent' - || (row.state === 'unknown' && hostedState === 'suspected_missing'); - const missing = ['absent', 'suspected_missing', 'confirmed_missing'].includes(row.state); - // The change mirror omits repeated checks, so raw absence cannot prove confirmation. - if ((confirmed || suspected) && row.complete === true) { - out.push({ entry, row, kind: confirmed ? 'lost' : 'suspected_missing' }); - } else if (missing || ['unknown', 'source_unavailable'].includes(row.state)) { - out.push({ entry, row, kind: 'cannot_say' }); + if (projection.uncertain || attempt.state === 'source_unavailable') { + out.push({ entry, row, kind: 'cannot_say', activity }); + } else if (['suspected_missing', 'confirmed_missing'].includes(projection.current_state)) { + out.push({ entry, row, kind: projection.current_state === 'confirmed_missing' ? 'lost' : 'suspected_missing', activity }); } } // Nothing is promoted automatically: whether a link that appeared is one we now EXPECT is // a judgement about a relationship, not about HTML. So this proposes the edit and stops. - if (entry.intent === 'wanted' && row.state === 'present') out.push({ entry, row, kind: 'appeared', suggest: { intent: 'expected' } }); + if (entry.intent === 'wanted' && !projection.uncertain && projection.current_state === 'present') out.push({ entry, row, kind: 'appeared', activity, suggest: { intent: 'expected' } }); } return out; } diff --git a/package-lock.json b/package-lock.json index 3eae9c3..335658c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentlinkops", - "version": "0.6.12", + "version": "0.6.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentlinkops", - "version": "0.6.12", + "version": "0.6.13", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/client": "2.2.0", diff --git a/package.json b/package.json index cd065c4..2d36f6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentlinkops", - "version": "0.6.12", + "version": "0.6.13", "description": "Backlink checks with dated evidence and portable results, optional repository ledgers, supplier-export imports and sync to AgentLinkOps.", "author": "AgentLinkOps (https://agentlinkops.com)", "license": "Apache-2.0", diff --git a/shared/index-observation-contract.js b/shared/index-observation-contract.js index 429466f..f06672f 100644 --- a/shared/index-observation-contract.js +++ b/shared/index-observation-contract.js @@ -4,7 +4,7 @@ export const indexConfigureInput=z.object({projectId:id,enabled:z.boolean(),dail export const indexInspectInput=z.object({connectionId:id,watchId:id,idempotencyKey:z.string().regex(/^[\x21-\x7e]{1,128}$/)}).strict(); export const indexHistoryInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict(); export const indexReceiptInput=z.object({projectId:id,watchId:id,receiptId:digest}).strict(); -export const indexEventsInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional()}).strict(); +export const indexEventsInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict(); export const indexSettingsInput=z.object({projectId:id}).strict(); 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(); 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 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()}); export const indexWebsiteQuota=z.object({windowStart:time,observedAt:time,admitted:z.number().int().nonnegative(),limit:z.number().int().positive(),remaining:z.number().int().nonnegative()}); export const indexHistoryResponse=z.object({observations:z.array(indexObservation),nextCursor:digest.nullable(),attemptSummary:indexAttemptSummary.optional(),websiteQuota:indexWebsiteQuota.optional()}).passthrough(); -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(); +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(); diff --git a/shared/monitoring-chronology-export.js b/shared/monitoring-chronology-export.js new file mode 100644 index 0000000..d85daf5 --- /dev/null +++ b/shared/monitoring-chronology-export.js @@ -0,0 +1,57 @@ +// Offline frozen chronology export. No hosted store, authentication, network or +// collection dependency belongs in the customer's CLI export closure. +const fail=(status,code,message)=>{throw Object.assign(new Error(message),{name:'ChronologyExportError',status,code});}; +const iso=value=>typeof value==='string'&&Number.isFinite(Date.parse(value))?new Date(value).toISOString():null; +const validIdentity=value=>{ + if(!value||typeof value!=='object'||Array.isArray(value))return false; + const id=v=>typeof v==='string'&&v.length>0&&v.length<=200&&!/[\s\x00-\x1f\x7f]/u.test(v); + 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;}}; + return ['workspaceId','projectId','watchId'].every(k=>id(value[k]))&&url(value.sourceUrl)&&url(value.targetUrl) + &&['exact','domain','subdomain','path'].includes(value.targetScope)&&(value.targetId===null||id(value.targetId)) + &&(value.localReference===null||typeof value.localReference==='string'&&value.localReference.length<=2048) + &&value.matchBasis==='configured_target_url'; +}; +const compare=(a,b)=>a.sortAt>b.sortAt?-1:a.sortAtb.id?-1:a.idArray.from(new Uint8Array(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(value))),b=>b.toString(16).padStart(2,'0')).join(''); +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:{}}); + +// Export only a supplied frozen dataset. This function never reads current DB +// state or fills a missing origin with today's projection. +export async function freezeChronology(pages,{localItems=[]}={}){ + if(!Array.isArray(pages)||!pages.length||pages.length>10000||!Array.isArray(localItems)||localItems.length>10000)fail(400,'INVALID_CHRONOLOGY_EXPORT','Provide bounded chronology pages.'); + 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.'); + const byId=new Map();let continuous=first.pageCursor===null; + for(let i=0;i12000:page.nextCursor!==null))fail(400,'INVALID_CHRONOLOGY_EXPORT','Each frozen page must declare whether another page exists with a consistent continuation cursor.'); + if(i&&pages[i-1].nextCursor!==page.pageCursor)continuous=false; + 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);} + } + 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);} + const complete=continuous&&pages.at(-1).hasMore===false,coverage=JSON.parse(JSON.stringify(pages.at(-1).coverage)); + if(localItems.length)coverage.local_submissions={status:'available',reason:'validated_local_reported_receipts',hasMore:false,providerAuthenticity:'not_established'}; + const items=[...byId.values()].sort(compare),gaps=[]; + for(const [stream,origin] of Object.entries(coverage)){ + 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};} + 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}:{})}); + } + if(!complete)gaps.push({stream:'pagination',reason:'partial_pagination'}); + 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}; + const frozen=JSON.parse(JSON.stringify(data));return {...frozen,contentHash:await sha256(JSON.stringify(frozen))}; +} + +// This is an offline normalization boundary, not server ingestion, collection or +// proof of provider authenticity. The hash uses the existing CLI receipt format. +export async function normalizeLocalSubmissions(rows,identity){ + if(!validIdentity(identity))fail(400,'INVALID_SUBMISSION_RECEIPT','An exact placement identity is required.'); + if(!Array.isArray(rows)||rows.length>10000)fail(400,'INVALID_SUBMISSION_RECEIPT','Provide bounded local submission receipts.'); + const fields=['v','id','project_id','url','channel','endpoint','submitted_at','http_status','evidence_sha256'],items=[],byId=new Map(); + for(const row of rows){ + 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.'); + 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).'); + 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.'); + 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; + 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}}); + } + return items.sort(compare); +} diff --git a/shared/monitoring-read-contract.js b/shared/monitoring-read-contract.js new file mode 100644 index 0000000..310a1d9 --- /dev/null +++ b/shared/monitoring-read-contract.js @@ -0,0 +1,41 @@ +import {z} from 'zod'; +const id=z.string().min(1).max(200),time=z.string().datetime(),nullableTime=time.nullable(),count=z.number().int().nonnegative(); +export const chronologyStreams=['context','link_observations','target_observations','link_events','target_events','index_observations','index_events','local_submissions']; +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(); +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')}); +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(); +// Additive retained-data projection; absence preserves older backend responses. +const nullableSeconds=z.number().nonnegative().nullable(); +const coverageProjection=z.object({ + supplied:count,active:count,paused:count,recordedHeld:count,schedulingCandidates:count,busy:count,dueCandidates:count, + eligibility:z.object({status:z.literal('unavailable'),eligible:z.null(),reason:z.literal('admission_not_rechecked')}), + denominatorMeaning:z.string(),freshnessWindow:z.string(),cadenceSeconds:z.object({minimum:count.nullable(),maximum:count.nullable()}), + oldestOverdueAt:nullableTime,oldestOverdueAgeSeconds:nullableSeconds,nextScheduledAt:nullableTime,nextEligibleAt:z.null(),nextEligibleReason:z.literal('admission_not_rechecked'), + neverAttempted:count,neverConclusive:count,currentUnknown:count,unknownReasons:z.record(z.string(),count),freshConclusive:count,staleConclusive:count, + 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(), + 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()}) +}); +const projectedJobs=z.object({ + retained:count,queued:count,running:count,succeeded:count,failed:count,cancelled:count,everClaimed:count,claimAttempts:count,retryClaims:count,retryExhausted:count,expiredRunningLeases:count, + oldestQueuedAt:nullableTime,oldestQueuedAgeSeconds:nullableSeconds,oldestRunningAdmissionAt:nullableTime,oldestRunningAdmissionAgeSeconds:nullableSeconds,latestAdmissionAt:nullableTime,latestCompletionAt:nullableTime, + scopedRetainedDeadLetters:count,deadLetterMeaning:z.string(),admissionToTerminal:z.object({samples:count,minimumSeconds:nullableSeconds,maximumSeconds:nullableSeconds,meaning:z.string()}), + startedAt:z.null(),queueWaitSeconds:z.null(),providerLatencySeconds:z.null(),timingReason:z.literal('first_start_and_provider_timing_not_retained'), + dispatch:z.object({outstanding:count,deferred:count,expiredSendingLeases:count,nextDispatchAt:nullableTime,meaning:z.string()}),historyMeaning:z.string() +}); +const projectedPlane=z.object({coverage:coverageProjection,jobs:projectedJobs}); +const indexProjection=z.discriminatedUnion('status',[ + z.object({status:z.literal('denied'),reason:z.string()}).strict(),z.object({status:z.literal('unavailable'),reason:z.string()}).strict(), + z.object({status:z.literal('available'),reason:z.string(),suppliedSourceWatches:count,activeSourceWatches:count, + 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), + 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()}) +]); +export const monitoringAssuranceProjection=z.object({schemaVersion:z.literal(1),source:projectedPlane,target:projectedPlane,index:indexProjection, + serviceTickWindow:z.object({expectedTicks:z.null(),completedTicks:z.null(),availabilityShare:z.null(),reason:z.literal('tick_history_not_retained')}), + capacity:z.object({dueOpportunities:z.null(),missedOpportunities:z.null(),checksPerEligibleWatch:z.null(),reason:z.literal('bounded_roster_opportunity_history_not_retained')}),meaning:z.string()}); +export const assuranceInput=z.object({projectId:id}).strict(); +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()}); +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(); +export const monitoringReadResponses={get_placement_chronology:chronologyResponse,get_monitoring_service_assurance:assuranceResponse}; +const at='2026-10-07T00:00:00.000Z',projectId='pr_example',workspaceId='ws_example'; +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}; +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.'}}; diff --git a/shared/response-contract.js b/shared/response-contract.js index b3bdf40..6d9e7e2 100644 --- a/shared/response-contract.js +++ b/shared/response-contract.js @@ -1,3 +1,4 @@ +import {chronologyResponse,assuranceResponse} from './monitoring-read-contract.js'; import {lifecycleOutput} from './lifecycle-contract.js'; import {searchConnectionsResponse,searchRowsResponse,searchSyncResponse,searchPropertiesResponse,searchStartResponse,searchConnection,searchImportResponse} from './search-connection-contract.js'; import {z} from 'zod'; @@ -55,6 +56,8 @@ export function restResponseContract(path,method='GET') { if(/^\/v1\/search-connections\/[^/]+\/disconnect$/.test(pathname)&&method==='POST')return z.object({disconnected:z.literal(true),retainedRows:z.literal(true)}); if(/^\/v1\/search-connections\/[^/]+$/.test(pathname)&&method==='DELETE')return z.object({forgotten:z.literal(true)}); if(method!=='GET')return objectResponse; + if(/^\/v1\/watches\/[^/]+\/chronology$/.test(pathname))return chronologyResponse; + if(/^\/v1\/projects\/[^/]+\/monitoring-assurance$/.test(pathname))return assuranceResponse; if(/^\/v1\/(?:watches|targets)\/[^/]+\/history$/.test(pathname))return historyResponse; if(/^\/v1\/watches\/[^/]+$/.test(pathname))return watchResponse; if(/^\/v1\/targets\/[^/]+$/.test(pathname))return targetResponse; diff --git a/skills/agentlinkops-connect/references/agentlinkops.md b/skills/agentlinkops-connect/references/agentlinkops.md index cce2c08..e7a2218 100644 --- a/skills/agentlinkops-connect/references/agentlinkops.md +++ b/skills/agentlinkops-connect/references/agentlinkops.md @@ -9,7 +9,7 @@ records. This page is the complete agent reference: the same text prints from full once per session, then use `describe` (or `exec describe` on MCP) for any command's exact schema. -Pack version 0.6.12. 139 commands in 15 toolsets. +Pack version 0.6.13. 147 commands in 16 toolsets. ## Install and connect (once per machine) @@ -269,18 +269,19 @@ The human-readable version is ## Tool reference -139 commands in 15 toolsets, generated from `src/tool-registry.js` (`npm run generate:tools`). Core on every view: `get_workspace`, `list_projects`, `list_link_watches`, `monitor_link`, `list_events`. The index verbs list the rest by name; `describe` is the source of truth for input schemas. Admission-gated today: prepare_domain_baseline, start_domain_baseline, request_competitor_inventory, request_domain_overview, discover_backlinks; these refuse before any persistence. +147 commands in 16 toolsets, generated from `src/tool-registry.js` (`npm run generate:tools`). Core on every view: `get_workspace`, `list_projects`, `list_link_watches`, `monitor_link`, `list_events`. The index verbs list the rest by name; `describe` is the source of truth for input schemas. Admission-gated today: prepare_domain_baseline, start_domain_baseline, request_competitor_inventory, request_domain_overview, discover_backlinks; these refuse before any persistence. | Toolset | Commands | What it covers | | --- | --- | --- | | `monitoring` | 17 | Watch earned links and destination URLs: create, list, update, import, export, recheck. | | `evidence` | 17 | What a check observed: histories, snapshots, change feeds, check jobs, published contacts. | | `rank` | 5 | Synthetic DataForSEO rank schedules and dated history; hosted collection disabled. | +| `licensed` | 6 | Licensed source-search evidence and charge custody; operator admission required. | | `discovery` | 16 | Import candidate rows, read stored runs, verify selected candidates, enroll them. | | `baseline` | 7 | Scoped backlink baselines; supplier admission disabled. | | `library` | 3 | Read and export the coverage-stated opportunity library. | | `competitors` | 17 | Competitor sets, dated inventories, scheduled refresh, gap and domain-mix reports. | -| `reports` | 7 | Profile, anchor and report summaries over the tracked dataset. | +| `reports` | 9 | Placement chronology, service assurance and backlink reports. | | `workspace` | 16 | Workspace, projects, usage, members, invitations, scratch-resource cleanup. | | `notifications` | 6 | Email notification preferences, previews, tests and deliveries. | | `webhooks` | 6 | Webhook endpoints, state, secrets and delivery records. | diff --git a/src/tool-views.js b/src/tool-views.js index 323d1ca..bdf356e 100644 --- a/src/tool-views.js +++ b/src/tool-views.js @@ -3,7 +3,7 @@ // script, the MCP router, the CLI index and the generators all read the same definitions. // Rules: docs/technical/AGENT-SURFACE-CONTRACT.md (DP-0036). -export const TOOLSETS = ['monitoring','evidence','rank','discovery','baseline','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','digests']; +export const TOOLSETS = ['monitoring','evidence','rank','licensed','discovery','baseline','library','competitors','reports','workspace','notifications','webhooks','admission','disavow','lifecycle','digests']; // One line per toolset: what the group is for. Printed by the site brief, the REST index and the // catalog snapshot so the same wording reaches every client; a toolset without a summary is a @@ -16,11 +16,12 @@ export const TOOLSET_SUMMARIES = { monitoring: 'Watch earned links and destination URLs: create, list, update, import, export, recheck.', evidence: 'What a check observed: histories, snapshots, change feeds, check jobs, published contacts.', rank: 'Synthetic DataForSEO rank schedules and dated history; hosted collection disabled.', + licensed: 'Licensed source-search evidence and charge custody; operator admission required.', discovery: 'Import candidate rows, read stored runs, verify selected candidates, enroll them.', baseline: 'Scoped backlink baselines; supplier admission disabled.', library: 'Read and export the coverage-stated opportunity library.', competitors: 'Competitor sets, dated inventories, scheduled refresh, gap and domain-mix reports.', - reports: 'Profile, anchor and report summaries over the tracked dataset.', + reports: 'Placement chronology, service assurance and backlink reports.', workspace: 'Workspace, projects, usage, members, invitations, scratch-resource cleanup.', notifications: 'Email notification preferences, previews, tests and deliveries.', webhooks: 'Webhook endpoints, state, secrets and delivery records.', @@ -57,10 +58,10 @@ export function oneLiner(description, max = 60) { } // entries: [{ name, description, toolset, tier, readOnly }]. Grouped, one line per command. -export function manifestText(entries, { max = 36 } = {}) { +export function manifestText(entries, { max = 28 } = {}) { const groups = new Map(TOOLSETS.map(t => [t, []])); for (const entry of entries) (groups.get(entry.toolset) ?? groups.set(entry.toolset, []).get(entry.toolset)).push(entry); - const lines = ['Commands by toolset (name: what it does). On /mcp use exec: `exec search `, `exec describe `, `exec call `; the CLI mirrors the verbs as `agentlinkops tools | describe | call`.']; + const lines = ['Commands by toolset. MCP: `exec search `, `exec describe `, `exec call `. CLI: `agentlinkops tools | describe | call`. Licensed supplier calls require operator admission.']; for (const [toolset, members] of groups) { if (!members.length) continue; lines.push(`\n${toolset}:`); diff --git a/test/cli-check.test.js b/test/cli-check.test.js index 388bab6..82f2083 100644 --- a/test/cli-check.test.js +++ b/test/cli-check.test.js @@ -166,7 +166,7 @@ test('the ledger maps to a watch, and a corrected URL keeps its history', () => test('status proposes a promotion and refuses to make it', async () => { const entries = [normalizeEntry({ id: 'lk_pitched1', intent: 'wanted', source: SOURCE, target: TARGET })]; - const rows = [observationRow('lk_pitched1', { state: 'present', reason: 'link_found', checkedAt: '2026-09-14T09:00:00.000Z', occurrences: [{}], evidence: { complete: true } })]; + const rows = [observationRow('lk_pitched1', { sourceUrl: SOURCE, targetUrl: TARGET, state: 'present', reason: 'link_found', checkedAt: '2026-09-14T09:00:00.000Z', occurrences: [{}], evidence: { complete: true } })]; const found = disagreements(entries, rows); assert.equal(found.length, 1); assert.equal(found[0].kind, 'appeared'); @@ -242,7 +242,7 @@ test('check is due-driven, so running it twice does not refetch the web', () => test('a repeated answer is activity, not a new observation', () => { const at = day => `2026-09-${String(day).padStart(2, '0')}T00:00:00.000Z`; - const present = day => observationRow('lk_stable01', { state: 'present', reason: 'link_found', checkedAt: at(day), occurrences: [{}], linkSignature: 'sig1', evidence: { complete: true } }); + const present = day => observationRow('lk_stable01', { sourceUrl: SOURCE, targetUrl: TARGET, targetScope: 'exact', state: 'present', reason: 'link_found', checkedAt: at(day), occurrences: [{}], linkSignature: 'sig1', evidence: { complete: true } }); let state = { v: 1, entries: {} }; let split = selectChanged([present(1)], state); @@ -259,11 +259,12 @@ test('a repeated answer is activity, not a new observation', () => { // A link still present whose anchor or rel changed IS an event. Comparing only the state // would file it as more of the same. - const changedAnchor = observationRow('lk_stable01', { state: 'present', reason: 'link_found', checkedAt: at(4), occurrences: [{}], linkSignature: 'sig2', evidence: { complete: true } }); + const changedAnchor = observationRow('lk_stable01', { sourceUrl: SOURCE, targetUrl: TARGET, targetScope: 'exact', state: 'present', reason: 'link_found', checkedAt: at(4), occurrences: [{}], linkSignature: 'sig2', evidence: { complete: true } }); assert.equal(selectChanged([changedAnchor], state).changed.length, 1); }); test('init, add, fmt and check work end to end from the command line', async t => { + t.mock.method(globalThis, 'fetch', () => assert.fail('a matching not-due placement must not fetch')); const space = await workspace(t); let result = await run(space.dir, ['add', '--source', SOURCE, '--target', TARGET, '--intent', 'expected', '--anchor', 'guide', '--ref', 'q3/1']); assert.equal(result.code, 0); @@ -286,7 +287,7 @@ test('init, add, fmt and check work end to end from the command line', async t = // A ledger with nothing due says so rather than silently doing nothing. Due-ness comes from // state.json, so a recorded observation alone does not make an entry not-due. - const row = observationRow(id, { state: 'present', reason: 'link_found', checkedAt: new Date().toISOString(), occurrences: [{}], evidence: { complete: true } }); + const row = observationRow(id, { sourceUrl: SOURCE, targetUrl: TARGET, targetScope: 'exact', state: 'present', reason: 'link_found', checkedAt: new Date().toISOString(), occurrences: [{}], evidence: { complete: true } }); await writeFile(space.observations, `${JSON.stringify(row)}\n`, 'utf8'); await writeFile(join(space.dir, '.agentlinkops/state.json'), `${JSON.stringify(applyRun({ v: 1, entries: {} }, [row]), null, 1)}\n`, 'utf8'); result = await run(space.dir, ['check']); @@ -310,7 +311,7 @@ test('check --json keeps stdout empty when there are no observations due and pre const space = await workspace(t); const fetchMock = t.mock.method(globalThis, 'fetch', () => assert.fail('an empty check must not fetch')); const entry = normalizeEntry({ id: 'lk_jsonempty', intent: 'expected', source: SOURCE, target: TARGET }); - const row = observationRow(entry.id, { state: 'present', reason: 'link_found', checkedAt: new Date().toISOString(), occurrences: [{}], evidence: { complete: true } }); + const row = observationRow(entry.id, { sourceUrl: SOURCE, targetUrl: TARGET, targetScope: 'exact', state: 'present', reason: 'link_found', checkedAt: new Date().toISOString(), occurrences: [{}], evidence: { complete: true } }); const scenarios = [ { name: 'empty ledger', entries: [], args: [], count: 0 }, { name: 'not due', entries: [entry], args: [], count: 1 }, diff --git a/test/cli-chronology.test.js b/test/cli-chronology.test.js new file mode 100644 index 0000000..b6830ba --- /dev/null +++ b/test/cli-chronology.test.js @@ -0,0 +1,76 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {mkdtemp,writeFile,readFile,rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {chronologyMain} from '../cli/chronology.js'; +const page={schemaVersion:1,identity:{workspaceId:'ws',projectId:'pr',watchId:'watch',sourceUrl:'https://publisher.example/page',targetUrl:'https://example.org/',targetScope:'exact',targetId:null,localReference:null,matchBasis:'configured_target_url'},asOf:'2026-10-07T00:00:00.000Z',snapshotId:'a'.repeat(64),items:[],pageCursor:null,nextCursor:'next',hasMore:true,coverage:{context:{status:'available',reason:'retained_rows_only',hasMore:false},link_events:{status:'denied',reason:'insufficient_scope'}},historyMeaning:'Retained records only.'}; +test('offline export is deterministic, explicit about partial paging and has no network or ledger dependency',async t=>{ + const dir=await mkdtemp(join(tmpdir(),'alo-chronology-'));t.after(()=>rm(dir,{recursive:true,force:true}));await writeFile(join(dir,'pages.json'),JSON.stringify([page])); + const args={_:['chronology','export'],tag:[],pages:'pages.json',out:'export.json'}; + await chronologyMain(args,{cwd:dir,out:()=>{},fetchImpl:()=>{throw Error('unexpected network');}}); + const first=await readFile(join(dir,'export.json'),'utf8');await chronologyMain({...args,out:'export2.json'},{cwd:dir,out:()=>{}});assert.equal(await readFile(join(dir,'export2.json'),'utf8'),first); + await assert.rejects(chronologyMain(args,{cwd:dir,out:()=>{}}),{code:'EEXIST'});assert.equal(await readFile(join(dir,'export.json'),'utf8'),first); + const input=await readFile(join(dir,'pages.json'),'utf8');await assert.rejects(chronologyMain({...args,out:'pages.json'},{cwd:dir,out:()=>{}}),{code:'EEXIST'});assert.equal(await readFile(join(dir,'pages.json'),'utf8'),input); + const data=JSON.parse(first);assert.equal(data.pagination.complete,false);assert.equal(data.pagination.reason,'partial_pagination');assert.equal(data.coverage.link_events.status,'denied');assert.match(data.contentHash,/^[a-f0-9]{64}$/); +}); +test('offline export refuses mixed placement snapshots before writing output',async t=>{ + const dir=await mkdtemp(join(tmpdir(),'alo-chronology-'));t.after(()=>rm(dir,{recursive:true,force:true}));await writeFile(join(dir,'pages.json'),JSON.stringify([page,{...page,identity:{...page.identity,watchId:'other'}}])); + await assert.rejects(chronologyMain({_:['chronology','export'],tag:[],pages:'pages.json',out:'export.json'},{cwd:dir,out:()=>{}}),{code:'CHRONOLOGY_EXPORT_SCOPE_MISMATCH'}); + await assert.rejects(readFile(join(dir,'export.json')),{code:'ENOENT'}); +}); + +test('offline export refuses absent or malformed placement identity before writing output',async t=>{ + const dir=await mkdtemp(join(tmpdir(),'alo-chronology-'));t.after(()=>rm(dir,{recursive:true,force:true})); + const malformed=[undefined,null,{}, {...page.identity,projectId:''},{...page.identity,targetScope:'any'},{...page.identity,sourceUrl:'file:///tmp/source'},{...page.identity,matchBasis:undefined}]; + for(const identity of malformed){ + await writeFile(join(dir,'pages.json'),JSON.stringify({...page,identity})); + await assert.rejects(chronologyMain({_:['chronology','export'],tag:[],pages:'pages.json',out:'export.json'},{cwd:dir,out:()=>{}}),{code:'INVALID_CHRONOLOGY_EXPORT'}); + await assert.rejects(readFile(join(dir,'export.json')),{code:'ENOENT'}); + } +}); + +test('new export path works on the checkout filesystem and refuses replacement',async t=>{ + const dir=await mkdtemp(new URL('../.chronology-export-',import.meta.url).pathname);t.after(()=>rm(dir,{recursive:true,force:true})); + await writeFile(join(dir,'pages.json'),JSON.stringify([page])); + const args={_:['chronology','export'],tag:[],pages:'pages.json',out:'export.json'}; + await chronologyMain(args,{cwd:dir,out:()=>{}});const first=await readFile(join(dir,'export.json'),'utf8'); + assert.match(JSON.parse(first).contentHash,/^[a-f0-9]{64}$/); + await assert.rejects(chronologyMain(args,{cwd:dir,out:()=>{}}),{code:'EEXIST'});assert.equal(await readFile(join(dir,'export.json'),'utf8'),first); +}); + +test('offline export rejects malformed terminal and continuation metadata without output or a new file',async t=>{ + const dir=await mkdtemp(join(tmpdir(),'alo-chronology-'));t.after(()=>rm(dir,{recursive:true,force:true})); + const last={...page,pageCursor:page.nextCursor,nextCursor:null,hasMore:false}; + const malformed=[ + {hasMore:false,nextCursor:'older'}, + {hasMore:true,nextCursor:null}, + {hasMore:true,nextCursor:''}, + {hasMore:true,nextCursor:'x'.repeat(12001)}, + {hasMore:true,nextCursor:42}, + {hasMore:'false',nextCursor:null}, + {hasMore:undefined,nextCursor:null}, + {hasMore:false,nextCursor:undefined}, + ]; + for(const pagination of malformed)for(const pages of [[{...page,...pagination}],[page,{...last,...pagination}]])for(const destination of [{out:'export.json'},{}]){ + const input=JSON.stringify(pages),output=[];await writeFile(join(dir,'pages.json'),input); + await assert.rejects(chronologyMain({_:['chronology','export'],tag:[],pages:'pages.json',...destination},{cwd:dir,out:value=>output.push(value),fetchImpl:()=>assert.fail('offline export must not fetch')}),{code:'INVALID_CHRONOLOGY_EXPORT'}); + assert.deepEqual(output,[]);await assert.rejects(readFile(join(dir,'export.json')),{code:'ENOENT'}); + assert.equal(await readFile(join(dir,'pages.json'),'utf8'),input); + } +}); + +test('offline continuation-only export remains partial and a legacy terminal page needs no invented readAt',async t=>{ + const dir=await mkdtemp(join(tmpdir(),'alo-chronology-'));t.after(()=>rm(dir,{recursive:true,force:true})); + const terminal={...page,pageCursor:'previous',nextCursor:null,hasMore:false}; + await writeFile(join(dir,'pages.json'),JSON.stringify([terminal])); + await chronologyMain({_:['chronology','export'],tag:[],pages:'pages.json',out:'continuation.json'},{cwd:dir,out:()=>{},fetchImpl:()=>assert.fail('offline export must not fetch')}); + const partial=JSON.parse(await readFile(join(dir,'continuation.json'),'utf8')); + assert.equal(partial.pagination.complete,false);assert.equal(partial.pagination.nextCursor,null);assert.equal(partial.pagination.pageCount,1); + assert.ok(partial.gaps.some(g=>g.stream==='pagination'&&g.reason==='partial_pagination')); + assert.equal('readAt' in partial,false);assert.equal(partial.asOf,page.asOf); + await writeFile(join(dir,'pages.json'),JSON.stringify([{...terminal,pageCursor:null}])); + await chronologyMain({_:['chronology','export'],tag:[],pages:'pages.json',out:'complete.json'},{cwd:dir,out:()=>{},fetchImpl:()=>assert.fail('offline export must not fetch')}); + const complete=JSON.parse(await readFile(join(dir,'complete.json'),'utf8')); + assert.equal(complete.pagination.complete,true);assert.equal(complete.pagination.nextCursor,null);assert.equal('readAt' in complete,false); +}); diff --git a/test/cli-client-resilience.test.js b/test/cli-client-resilience.test.js index da32e86..5895ea9 100644 --- a/test/cli-client-resilience.test.js +++ b/test/cli-client-resilience.test.js @@ -11,14 +11,14 @@ const unavailable=(status,headers={})=>Response.json({error:{code:status===429?' {status,headers:{'X-Request-ID':`req-${status}`,...headers}}); const client=(fetchImpl,options={})=>createClient({origin:ORIGIN,token:TOKEN,fetchImpl,...options}); -test('the REST client sends the 0.6.12 release User-Agent', async () => { +test('the REST client sends the 0.6.13 release User-Agent', async () => { const requests = []; const result = await client(async (url, init) => { const request = new Request(url, init); requests.push(request); assert.equal(request.url, ORIGIN + '/v1/watches'); assert.equal(request.method, 'GET'); - assert.equal(request.headers.get('user-agent'), 'agentlinkops-cli/0.6.12'); + assert.equal(request.headers.get('user-agent'), 'agentlinkops-cli/0.6.13'); assert.equal(request.headers.get('authorization'), 'Bearer ' + TOKEN); return page(); }).listWatches(); diff --git a/test/cli-doctor-writer.test.js b/test/cli-doctor-writer.test.js index 537b0dd..5b75015 100644 --- a/test/cli-doctor-writer.test.js +++ b/test/cli-doctor-writer.test.js @@ -9,7 +9,7 @@ import {join} from 'node:path'; if (typeof mock.module === 'function') mock.module('../cli/native-lock.js', {namedExports: {nativeLock: async () => ({acquire: () => true})}}); const {doctorMain} = await import('../cli/doctor.js'); const {acquireSyncLock} = await import('../cli/sync-lock.js'); -const {writeState} = await import('../cli/state.js'); +const {writeState, STATE_VERSION} = await import('../cli/state.js'); const {main} = await import('../cli/main.js'); async function ledger(t) { @@ -19,7 +19,7 @@ async function ledger(t) { await writeFile(join(dir, 'links.jsonl'), JSON.stringify({id: 'lk_sameid01', intent: 'expected', source: 'https://publisher.example/a', target: 'https://customer.example/b'}) + '\n'); await writeFile(join(dir, 'config.json'), JSON.stringify({cloud: {origin: 'https://api.example'}})); const statePath = join(dir, 'state.json'); - const before = {v: 2, entries: {}, watches: {lk_sameid01: 'old'}, cursors: {events: 'old-source', target_events: 'old-target'}, custom: {site: cwd}}; + const before = {v: STATE_VERSION, entries: {}, watches: {lk_sameid01: 'old'}, cursors: {events: 'old-source', target_events: 'old-target'}, custom: {site: cwd}}; await writeState(statePath, before); return {cwd, dir, statePath, before}; } diff --git a/test/cli-fleet.test.js b/test/cli-fleet.test.js index 8d4960e..d833153 100644 --- a/test/cli-fleet.test.js +++ b/test/cli-fleet.test.js @@ -13,10 +13,10 @@ async function scratch(t) { } const entry = (id, intent, source) => `${JSON.stringify({ id, intent, source, target: 'https://me.example/', scope: 'domain' })}\n`; -const observation = (id, state, checked_at = '2026-09-11T05:00:00.000Z') => `${JSON.stringify({ +const observation = (id, state, checked_at = '2026-09-11T05:00:00.000Z', sourceUrl = `https://${id}.example/x`) => `${JSON.stringify({ id, checked_at, state, reason: state === 'present' ? 'link_found' : 'no_matching_link_in_complete_html', occurrences: state === 'present' ? 1 : 0, complete: true, checker_version: '1', source: 'local', evidence_key: null, - result: { state, reason: 'x', sourceUrl: `https://${id}.example/x`, directives: { noindex: false } }, + result: { state, reason: 'x', sourceUrl, targetUrl: 'https://me.example/', targetScope: 'domain', directives: { noindex: false } }, })}\n`; test('fleet summarises each project on its own and never merges intent', async t => { @@ -27,7 +27,7 @@ test('fleet summarises each project on its own and never merges intent', async t // merely wanted in another. Two files, two ids, and the summary must report both. await writeFile(join(dir, 'a', 'links.jsonl'), entry('lk_expected1', 'expected', 'https://platform.example/a') + entry('lk_wantedaa', 'wanted', 'https://other.example/')); await writeFile(join(dir, 'b', 'links.jsonl'), entry('lk_wantedbb', 'wanted', 'https://platform.example/b')); - await writeFile(join(dir, 'a', 'observations.jsonl'), observation('lk_expected1', 'present')); + await writeFile(join(dir, 'a', 'observations.jsonl'), observation('lk_expected1', 'present', undefined, 'https://platform.example/a')); const summary = await fleetSummary( [{ name: 'a', ledger: join(dir, 'a', 'links.jsonl') }, { name: 'b', ledger: join(dir, 'b', 'links.jsonl') }], diff --git a/test/cli-freshness-surface.test.js b/test/cli-freshness-surface.test.js new file mode 100644 index 0000000..577f8c7 --- /dev/null +++ b/test/cli-freshness-surface.test.js @@ -0,0 +1,83 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {mkdtemp, mkdir, readFile, readdir, rm, writeFile} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import {main} from '../cli/main.js'; +import {normalizeEntry,serializeLedger} from '../cli/ledger.js'; +import {observationRow} from '../cli/mirror.js'; +import {applyRun} from '../cli/state.js'; + +const at=minutes=>new Date(Date.parse('2026-10-07T00:00:00.000Z')+minutes*60_000).toISOString(); +const entry=id=>normalizeEntry({id,intent:'expected',source:`https://publisher.example/${id}`,target:'https://customer.example/guide',scope:'exact'}); +const row=(entry,state,minutes)=>observationRow(entry.id,{sourceUrl:entry.source,targetUrl:entry.target,targetScope:entry.scope,state,checkedAt:at(minutes), + reason:state==='unknown'?'source_http_403':state==='present'?'link_found':'no_matching_link_in_complete_html', + occurrences:state==='present'?[{targetUrl:entry.target,anchor:'Guide',rel:[]}]:[],linkSignature:state==='present'?'stable-present':null, + evidence:{complete:state!=='unknown',method:'http_html',checkerVersion:'fixture-1',sha256:'a'.repeat(64)}}); +const invoke=async(cwd,argv)=>{ + const out=[],err=[]; + const code=await main(argv,{cwd,env:{},out:value=>out.push(String(value)),err:value=>err.push(String(value)),fetchImpl:()=>assert.fail('local freshness reads must not fetch')}); + return {code,out:out.join('\n'),err:err.join('\n')}; +}; +async function bytes(dir){ + const found={}; + const visit=async(path=dir,prefix='')=>{ + for(const item of (await readdir(path,{withFileTypes:true})).sort((a,b)=>a.nameb.name?1:0)){ + const absolute=join(path,item.name),relative=prefix+item.name; + if(item.isDirectory())await visit(absolute,relative+'/');else found[relative]=(await readFile(absolute)).toString('base64'); + } + }; + await visit();return found; +} +async function workspace(t,entries,observations,state){ + const cwd=await mkdtemp(join(tmpdir(),'alo-freshness-surface-'));t.after(()=>rm(cwd,{recursive:true,force:true})); + const dir=join(cwd,'.agentlinkops');await mkdir(dir); + const paths={ledger:join(dir,'links.jsonl'),observations:join(dir,'observations.jsonl'),state:join(dir,'state.json')}; + await writeFile(paths.ledger,serializeLedger(entries));await writeFile(paths.observations,observations.map(value=>JSON.stringify(value)).join('\n')+'\n'); + await writeFile(paths.state,JSON.stringify(state)+'\n');return {cwd,paths}; +} + +test('actual status --all joins latest activity while legacy array, receipt dates and customer bytes stay stable',async t=>{ + t.mock.method(globalThis,'fetch',()=>assert.fail('status/report/fleet never fetch')); + const healthy=entry('lk_healthy01'),blocked=entry('lk_blocked01'),corrected=entry('lk_corrected1'),missing=entry('lk_missing01'); + const retained=[row(healthy,'present',0),row(blocked,'present',0),row(blocked,'unknown',100),row(corrected,'present',0),row(missing,'absent',40)]; + const activity=applyRun({v:2,entries:{},cursors:{events:'source-keep',target_events:'target-keep'},doctor:{keep:true},future:{keep:['custom']}}, + [row(healthy,'present',0),row(healthy,'present',50),row(blocked,'present',0),row(blocked,'present',50),row(blocked,'unknown',100),row(corrected,'present',0),row(missing,'absent',40),row(missing,'absent',90)]); + activity.entries[healthy.id].futureEntry={keep:17}; + const f=await workspace(t,[healthy,blocked,{...corrected,scope:'domain'},missing],retained,activity),before=await bytes(f.cwd); + const legacy=await invoke(f.cwd,['status','--json']);assert.equal(legacy.code,0);assert.equal(legacy.err,''); + const disagreements=JSON.parse(legacy.out);assert.equal(Array.isArray(disagreements),true); + assert.deepEqual(disagreements.map(item=>[item.entry.id,item.kind]),[[blocked.id,'cannot_say'],[corrected.id,'never_checked'],[missing.id,'suspected_missing']]); + const all=await invoke(f.cwd,['status','--all','--json']);assert.equal(all.code,0);assert.equal(all.err,''); + const result=JSON.parse(all.out);assert.deepEqual(result.disagreements,disagreements);assert.equal(result.placements.length,4); + const healthyView=result.placements.find(item=>item.id===healthy.id),blockedView=result.placements.find(item=>item.id===blocked.id),correctedView=result.placements.find(item=>item.id===corrected.id); + assert.equal(healthyView.current_state,'present');assert.equal(healthyView.latest_attempt.checked_at,at(50));assert.equal(healthyView.evidence_observed_at,at(0)); + assert.equal(healthyView.last_link_verification.evidence_reference,null); + assert.equal(blockedView.current_state,'present');assert.equal(blockedView.uncertain,true);assert.equal(blockedView.latest_attempt.checked_at,at(100)); + assert.equal(blockedView.latest_attempt.reason,'source_http_403');assert.equal(blockedView.last_successful_observation.checked_at,at(50)); + assert.equal(correctedView.current_state,'unchecked');assert.equal(correctedView.identity_status,'not_matched');assert.equal(correctedView.latest_attempt,null); + assert.equal((await invoke(f.cwd,['status','--all','--json'])).out,all.out,'the read has no current-time input or mutation'); + const human=await invoke(f.cwd,['status','--all']);assert.equal(human.code,0);assert.match(human.out,/Latest attempt: 2026-10-07T01:40:00\.000Z \(source_http_403\)/); + assert.ok(human.out.includes(`${healthy.id}: latest attempt ${at(50)}; last conclusive ${at(50)}; last link verification ${at(50)}`)); + const report=await invoke(f.cwd,['report','--json','--as-of','2026-10-07']);assert.equal(report.code,0); + const dataset=JSON.parse(report.out),reportHealthy=dataset.rows.find(item=>item.id===healthy.id),reportBlocked=dataset.rows.find(item=>item.id===blocked.id); + assert.equal(reportHealthy.checked_at,at(0));assert.deepEqual(reportHealthy.activity.latest_attempt,healthyView.latest_attempt); + assert.equal(reportBlocked.state,'unknown');assert.equal(reportBlocked.current_state,'present');assert.equal(dataset.coverage.identity_unmatched_entries,1); + const fleet=await invoke(f.cwd,['fleet','--project',`isolated=${f.paths.ledger}`,'--observations-of',`isolated=${f.paths.observations}`,'--state-of',`isolated=${f.paths.state}`,'--json']); + assert.equal(fleet.code,0);const project=JSON.parse(fleet.out).projects[0];assert.equal(project.last_attempt_at,at(100));assert.equal(project.last_link_verification_at,at(90)); + assert.equal(project.uncertain_entries,1);assert.equal(project.coverage.identity_unmatched_entries,1); + assert.deepEqual(await bytes(f.cwd),before,'all read surfaces preserve ledger, mirror, activity, cursors and unknown keys byte-for-byte'); +}); + +test('actual main distinguishes state-only checks from unavailable receipts without inventing a locator',async t=>{ + t.mock.method(globalThis,'fetch',()=>assert.fail('state-only reads never fetch')); + const selected=entry('lk_stateonly1'),state=applyRun({v:2,entries:{},cursors:{events:'keep',target_events:'keep-too'}},[row(selected,'present',50)]); + const f=await workspace(t,[selected],[],state),before=await bytes(f.cwd); + const status=await invoke(f.cwd,['status','--all','--json']);assert.equal(status.code,0);const value=JSON.parse(status.out); + assert.deepEqual(value.disagreements,[]);assert.equal(value.placements[0].latest_attempt.checked_at,at(50)); + assert.equal(value.placements[0].evidence_observed_at,null);assert.equal(value.placements[0].last_link_verification.evidence_reference,null); + const report=await invoke(f.cwd,['report','--json','--as-of','2026-10-07']),dataset=JSON.parse(report.out); + assert.equal(report.code,0);assert.equal(dataset.rows[0].evidence,null);assert.equal(dataset.rows[0].current_state,'present'); + assert.equal(dataset.coverage.checked,1);assert.equal(dataset.coverage.never_checked,0);assert.equal(dataset.coverage.retained_receipt_entries,0); + assert.deepEqual(await bytes(f.cwd),before); +}); diff --git a/test/cli-freshness.test.js b/test/cli-freshness.test.js new file mode 100644 index 0000000..2a3e4d2 --- /dev/null +++ b/test/cli-freshness.test.js @@ -0,0 +1,215 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { applyRun, selectChanged, lastCheckedMap, upgradeState, STATE_VERSION } from '../cli/state.js'; +import { observationRow, compactionPlan } from '../cli/mirror.js'; +import { placementIdentity, projectPlacement } from '../cli/placement-projection.js'; +import { disagreements } from '../cli/status.js'; +import { freezeDataset, renderReport, datasetDigest } from '../cli/report.js'; +import { fleetSummary } from '../cli/fleet.js'; + +const entry = { id: 'lk_fresh001', intent: 'expected', source: 'https://publisher.example/a', target: 'https://customer.example/', scope: 'exact', cadence: 'daily' }; +const at = minutes => new Date(Date.parse('2026-10-07T00:00:00.000Z') + minutes * 60_000).toISOString(); +const row = (state, minutes, changes = {}) => observationRow(entry.id, { + sourceUrl: entry.source, targetUrl: entry.target, targetScope: entry.scope, state, + reason: state === 'unknown' ? 'timeout' : state === 'present' ? 'link_found' : state, + checkedAt: at(minutes), linkSignature: state === 'present' ? 'present-signature' : null, + occurrences: state === 'present' ? [{ anchor: 'Guide', rel: [], targetUrl: entry.target }] : [], + evidence: { complete: state !== 'unknown', sha256: String(minutes).padStart(64, '0'), method: 'http_html', checkerVersion: '1' }, ...changes, +}); +const empty = () => ({ v: 2, entries: {}, cursors: { events: 'source-cursor', target_events: 'target-cursor' }, future: { keep: true } }); + +test('the existing frozen v2 report still renders byte-for-byte identically', async () => { + const fixture = JSON.parse(await readFile(new URL('./fixtures/report-v2-snapshot.json', import.meta.url), 'utf8')); + assert.equal(fixture.dataset.v, 2); + assert.equal(createHash('sha256').update(renderReport(fixture.dataset)).digest('hex'), fixture.html_sha256); +}); + +test('unchanged success advances activity without manufacturing a retained receipt or change', () => { + const original = row('present', 0), unchanged = row('present', 50); + const before = applyRun(empty(), [original]); + const selected = selectChanged([unchanged], before); + assert.equal(selected.changed.length, 0); + assert.equal(selected.repeated.length, 1); + const state = applyRun(before, [unchanged]); + const view = projectPlacement(entry, [original], { state }); + assert.equal(view.evidence_observed_at, at(0)); + assert.equal(view.last_successful_observation.checked_at, at(50)); + assert.equal(view.last_link_verification.checked_at, at(50)); + assert.equal(view.last_link_verification.evidence_reference, null, 'an omitted receipt cannot be located'); + assert.equal(view.last_link_verification.content_hash, unchanged.result.evidence.sha256); + const dataset = freezeDataset([entry], [original], { asOf: '2026-10-07', activityState: state }); + assert.equal(dataset.rows[0].checked_at, at(0)); + assert.equal(dataset.rows[0].evidence.content_hash, original.result.evidence.sha256); + assert.equal(dataset.rows[0].activity.latest_attempt.checked_at, at(50)); + assert.equal(renderReport(dataset), renderReport(JSON.parse(JSON.stringify(dataset)))); + assert.notEqual(datasetDigest(dataset), datasetDigest(freezeDataset([entry], [original], { asOf: '2026-10-07', activityState: before }))); +}); + +test('blocked attempts retain the last conclusive observation and verified link alongside unknown reason/date', () => { + const original = row('present', 0), unchanged = row('present', 50); + for (const reason of ['source_http_403', 'timeout', 'robots_disallowed', 'access_challenge']) { + const blocked = row('unknown', 100, { reason }); + const state = applyRun(applyRun(empty(), [original, unchanged]), [blocked]); + const view = projectPlacement(entry, [original, blocked], { state }); + assert.equal(view.current_state, 'present'); + assert.equal(view.uncertain, true); + assert.equal(view.latest_attempt.reason, reason); + assert.equal(view.latest_attempt.checked_at, at(100)); + assert.equal(view.last_successful_observation.checked_at, at(50)); + assert.equal(view.last_link_verification.state, 'present'); + const mismatch = disagreements([entry], [original, blocked], { state })[0]; + assert.equal(mismatch.kind, 'cannot_say'); + assert.equal(mismatch.activity.last_link_verification.checked_at, at(50)); + const report = freezeDataset([entry], [original, blocked], { asOf: '2026-10-07', activityState: state }); + assert.equal(report.rows[0].state, 'unknown', 'raw retained receipt is never rewritten'); + assert.equal(report.rows[0].current_state, 'present'); + assert.match(renderReport(report), /latest attempt inconclusive/); + } +}); + +test('source unavailable advances conclusive freshness but cannot replace link verification or prove loss', () => { + const original = row('present', 0), unavailable = row('source_unavailable', 100, { httpStatus: 410 }); + const state = applyRun(empty(), [original, unavailable]); + const view = projectPlacement(entry, [original, unavailable], { state }); + assert.equal(view.current_state, 'source_unavailable'); + assert.equal(view.last_successful_observation.checked_at, at(100)); + assert.equal(view.last_link_verification.checked_at, at(0)); + assert.equal(disagreements([entry], [original, unavailable], { state })[0].kind, 'cannot_say'); +}); + +test('identity-bound activity without a retained receipt proves a check, without inventing receipt evidence', () => { + const state = applyRun(empty(), [row('present', 50)]); + const dataset = freezeDataset([entry], [], { asOf: '2026-10-07', activityState: state }); + assert.equal(dataset.rows[0].state, 'unchecked', 'the raw receipt field remains empty'); + assert.equal(dataset.rows[0].evidence, null); + assert.equal(dataset.rows[0].current_state, 'present'); + assert.equal(dataset.rows[0].activity.latest_attempt.evidence_reference, null); + assert.equal(dataset.rows[0].activity.last_link_verification.evidence_reference, null); + assert.equal(dataset.coverage.checked, 1); + assert.equal(dataset.coverage.never_checked, 0); + assert.equal(dataset.coverage.retained_receipt_entries, 0); + assert.match(renderReport(dataset), /No retained observation/); +}); + +test('raw local absence stays suspected after repeats; authoritative hosted confirmation survives compaction', () => { + const first = row('absent', 1), repeated = row('absent', 40); + const local = applyRun(empty(), [first, repeated]); + assert.equal(disagreements([entry], [first], { state: local })[0].kind, 'suspected_missing'); + assert.equal(projectPlacement(entry, [first], { state: local }).first_present, null); + const present = row('present', 0); + const confirmed = { ...row('absent', 60, { watchState: 'confirmed_missing', checkState: 'absent' }), source: 'cloud' }; + const history = compactionPlan([present, first, confirmed]).keep; + assert.equal(disagreements([entry], history)[0].kind, 'lost'); + const blocked = row('unknown', 100); + assert.equal(projectPlacement(entry, [...history, blocked]).current_state, 'confirmed_missing'); + assert.equal(disagreements([entry], [...history, blocked])[0].kind, 'cannot_say'); + assert.equal(disagreements([entry], [...history, row('absent', 110)])[0].kind, 'suspected_missing', + 'a newer independent local absence does not inherit the old hosted confirmation'); + assert.equal(projectPlacement(entry, [...history, row('present', 120)]).current_state, 'present'); +}); + +test('duplicate, out-of-order and invalid timestamps do not regress activity or increment checks', () => { + const recent = row('present', 50); + const state = applyRun(empty(), [recent]); + const retry = [recent, row('unknown', 40), { ...row('absent', 60), checked_at: 'invalid' }]; + assert.deepEqual(applyRun(state, retry), state); + const selected = selectChanged(retry, state); + assert.equal(selected.changed.length, 0); + assert.equal(selected.repeated.length, 0); + assert.equal(selected.ignored.length, 3); + assert.deepEqual(state.cursors, empty().cursors); + assert.deepEqual(state.future, { keep: true }); +}); + +test('equal-time retained receipt selection is deterministic and matches its projected evidence', () => { + const present = row('present', 50), absent = row('absent', 50, { evidence: present.result.evidence }); + const forward = freezeDataset([entry], [present, absent], { asOf: '2026-10-07' }); + const reverse = freezeDataset([entry], [absent, present], { asOf: '2026-10-07' }); + assert.deepEqual(forward, reverse); + assert.equal(forward.rows[0].state, forward.rows[0].activity.latest_attempt.state); + const otherAnchor = structuredClone(present); + otherAnchor.result.occurrences[0].anchor = 'Different'; + assert.deepEqual(freezeDataset([entry], [present, otherAnchor], { asOf: '2026-10-07' }), + freezeDataset([entry], [otherAnchor, present], { asOf: '2026-10-07' })); + const malformed = { id: entry.id, intent: 'expected' }; + assert.equal(projectPlacement(malformed, [{ ...present, result: {} }]).latest_attempt, null); +}); + +test('source, target and scope corrections cannot inherit activity, presence or cadence from the same ledger id', () => { + const original = row('present', 0); + const old = applyRun(empty(), [original]); + old.entries[entry.id].futureEntry = { keep: true }; + for (const correction of [{ source: 'https://publisher.example/b' }, { target: 'https://customer.example/new' }, { scope: 'domain' }]) { + const corrected = { ...entry, ...correction }; + assert.equal(lastCheckedMap(old, [corrected]).has(entry.id), false); + const unbound = projectPlacement(corrected, [original], { state: old }); + assert.equal(unbound.current_state, 'unchecked'); + assert.equal(unbound.first_present, null); + const blocked = row('unknown', 50, { sourceUrl: corrected.source, targetUrl: corrected.target, targetScope: corrected.scope }); + assert.equal(selectChanged([blocked], old).changed.length, 1); + const updated = applyRun(old, [blocked]); + const activity = updated.entries[entry.id]; + assert.equal(activity.last_placement, placementIdentity(corrected)); + assert.equal(activity.first_present, null); + assert.equal(activity.last_successful_observation, null); + assert.equal(activity.last_link_verification, null); + assert.deepEqual(activity.futureEntry, { keep: true }); + } +}); + +test('legacy migration preserves unknown keys, cursors and truthful missing last-success provenance', () => { + const original = row('present', 0); + const legacy = { ...empty(), entries: { [entry.id]: { last_checked: at(100), last_state: 'unknown', last_complete: false, last_placement: placementIdentity(entry), future: 'keep' } } }; + const migrated = upgradeState(legacy); + assert.equal(migrated.v, STATE_VERSION); + assert.equal(migrated.entries[entry.id].future, 'keep'); + const view = projectPlacement(entry, [original], { state: migrated }); + assert.equal(view.latest_attempt.checked_at, at(100)); + assert.equal(view.last_successful_observation.checked_at, at(0), 'unretained prior rechecks cannot be reconstructed'); + assert.equal(view.last_successful_observation.method, 'http_html'); + const completeLegacy = structuredClone(migrated); + Object.assign(completeLegacy.entries[entry.id], { last_state: 'present', last_complete: true }); + const fresh = projectPlacement(entry, [original], { state: completeLegacy }); + assert.equal(fresh.last_successful_observation.checked_at, at(100)); + assert.equal(fresh.last_successful_observation.method, null); + assert.equal(fresh.last_successful_observation.evidence_reference, null); +}); + +test('method and completeness changes are retained even when link signature is unchanged', () => { + const original = row('present', 0); + const state = applyRun(empty(), [original]); + const browser = row('present', 50, { evidence: { complete: true, method: 'browser_html', checkerVersion: '1', sha256: 'b'.repeat(64) } }); + assert.equal(selectChanged([browser], state).changed.length, 1); + const incomplete = row('present', 50, { evidence: { complete: false, method: 'http_html', checkerVersion: '1' } }); + assert.equal(selectChanged([incomplete], state).changed.length, 1); + assert.equal(projectPlacement(entry, [original], { state: applyRun(state, [incomplete]) }).uncertain, true); +}); + +test('fleet reads explicit per-project activity and keeps scope/denominators separate', async t => { + const dir = await mkdtemp(join(tmpdir(), 'alo-freshness-')); + t.after(() => rm(dir, { recursive: true, force: true })); + const original = row('present', 0); + const projects = [], observationsOf = {}, statesOf = {}; + for (const [name, minutes] of [['a', 50], ['b', 90]]) { + const ledger = join(dir, `${name}-links.jsonl`); + await writeFile(ledger, JSON.stringify(entry) + '\n' + JSON.stringify({ ...entry, id: 'lk_retired1', intent: 'retired' }) + '\n'); + observationsOf[name] = join(dir, `${name}-observations.jsonl`); + await writeFile(observationsOf[name], JSON.stringify(original) + '\n' + JSON.stringify({ ...original, id: 'lk_retired1' }) + '\n'); + statesOf[name] = join(dir, `${name}-state.json`); + await writeFile(statesOf[name], JSON.stringify(applyRun(empty(), [original, row('present', minutes)]))); + projects.push({ name, ledger }); + } + const summary = await fleetSummary(projects, { observationsOf, statesOf }); + assert.equal(summary.projects[0].last_attempt_at, at(50)); + assert.equal(summary.projects[1].last_attempt_at, at(90)); + assert.equal(summary.projects[0].last_observation, at(0)); + assert.equal(summary.totals.earned, 2); + assert.equal(summary.totals.never_checked, 0, 'retired history does not make a negative active denominator'); + const noActivity = await fleetSummary(projects, { observationsOf }); + assert.equal(noActivity.projects[0].activity_available, false); + assert.equal(noActivity.projects[0].coverage.activity_input, 'not_supplied'); +}); diff --git a/test/cli-loss-evidence.test.js b/test/cli-loss-evidence.test.js index 32c7ab9..e7f8478 100644 --- a/test/cli-loss-evidence.test.js +++ b/test/cli-loss-evidence.test.js @@ -17,7 +17,7 @@ import { verifyLink } from '../src/verifier/index.js'; const entry = normalizeEntry({ id: 'lk_evidence01', intent: 'expected', source: SOURCE, target: TARGET }); const timestamp = index => new Date(Date.UTC(2026, 8, 30, 0, index)).toISOString(); function observation(state, complete = true, index = 0, source = 'local') { - return observationRow(entry.id, { state, checkedAt: timestamp(index), reason: `fixture_${state}`, + return observationRow(entry.id, { sourceUrl: SOURCE, targetUrl: TARGET, targetScope: 'exact', state, checkedAt: timestamp(index), reason: `fixture_${state}`, occurrences: state === 'present' ? [{}] : [], evidence: { complete, checkerVersion: 'fixture_v1' } }, { source }); } function hosted(state, uncertain = false) { @@ -104,7 +104,7 @@ test('real status retains hosted confirmed-missing evidence but refuses incomple const f = await workspace(t, [observation('confirmed_missing', true, 0, 'cloud')]); const confirmed = await invoke(f.dir, ['status', '--json']); assert.equal(JSON.parse(confirmed.out)[0].kind, 'lost'); - assert.match((await invoke(f.dir, ['status'])).out, /\bLOST \(1\)/); + assert.match((await invoke(f.dir, ['status'])).out, /confirmed missing \(1\)/); await writeObservations(f.mirror, [observation('confirmed_missing', false, 1, 'cloud')]); const before = await bytes(f.dir), incomplete = await invoke(f.dir, ['status', '--json']); assert.equal(JSON.parse(incomplete.out)[0].kind, 'cannot_say'); @@ -165,7 +165,7 @@ test('actual hosted snapshot normalization preserves confirmed watch state throu const status = await invoke(f.dir, ['status', '--json']), human = await invoke(f.dir, ['status']); const found = JSON.parse(status.out)[0]; assert.equal(found.kind, 'lost'); assert.equal(found.row.state, 'absent'); - assert.equal(found.row.result.watchState, 'confirmed_missing'); assert.match(human.out, /\bLOST \(1\)/); + assert.equal(found.row.result.watchState, 'confirmed_missing'); assert.match(human.out, /confirmed missing \(1\)/); assert.deepEqual(await bytes(f.dir), before); }); diff --git a/test/fixtures/report-v2-snapshot.json b/test/fixtures/report-v2-snapshot.json new file mode 100644 index 0000000..9bdc663 --- /dev/null +++ b/test/fixtures/report-v2-snapshot.json @@ -0,0 +1,48 @@ +{ + "dataset": { + "v": 2, + "title": "Backlink report", + "as_of": "2026-09-11", + "include_notes": false, + "rows": [ + { + "id": "lk_legacy01", + "intent": "expected", + "source": "https://publisher.example/a", + "target": "https://customer.example/", + "scope": "exact", + "expected_anchor": null, + "state": "present", + "reason": "link_found", + "occurrences": 0, + "checked_at": "2026-09-10T00:00:00.000Z", + "checked_by": "local", + "anchor": null, + "rel": [], + "evidence": { + "observed_at": "2026-09-10T00:00:00.000Z", + "content_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "snapshot_key": null, + "origin": "local", + "checker_version": null, + "method": "static_html", + "hosted_observation_id": null, + "locator": "agentlinkops:evidence/1/link/mirror/lk_legacy01/local/2026-09-10T00%3A00%3A00.000Z" + }, + "note": null, + "ref": null + } + ], + "totals": { + "entries": 1, + "present": 1 + }, + "coverage": { + "checked": 1, + "never_checked": 0, + "observed_from": "2026-09-10T00:00:00.000Z", + "observed_to": "2026-09-10T00:00:00.000Z" + } + }, + "html_sha256": "e969ed266d8c0f643a05618ccc0ff26573d0355792e5273daa1e3f9b7e4db988" +}