agentlinkops 0.6.13 → 0.6.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/CHANGELOG.md +25 -2
- package/PACK-ARTIFACT.json +16 -0
- package/PACKS.json +591 -0
- package/README.md +34 -48
- package/cli/adapters/suppliers.js +1 -1
- package/cli/agent-setup.js +101 -17
- package/cli/campaign-bridge.js +325 -0
- package/cli/campaign-outcomes.js +107 -0
- package/cli/catalog.js +45702 -18674
- package/cli/check.js +40 -10
- package/cli/client.js +2 -1
- package/cli/commands.js +17 -8
- package/cli/connection.js +92 -18
- package/cli/contact-adoption.js +91 -0
- package/cli/context.js +13 -2
- package/cli/doctor.js +15 -4
- package/cli/import.js +7 -1
- package/cli/key-store.js +115 -0
- package/cli/ledger.js +1 -0
- package/cli/local-result.js +41 -6
- package/cli/main.js +51 -12
- package/cli/mcp-client.js +2 -2
- package/cli/migration.js +308 -0
- package/cli/prompt.js +25 -0
- package/cli/public-http.js +168 -0
- package/cli/state.js +14 -0
- package/cli/sync.js +1 -1
- package/package.json +7 -8
- package/references/citation-panels.md +2 -0
- package/references/cli.md +2 -2
- package/references/disavow.md +1 -1
- package/references/discovery-handoff.md +1 -1
- package/references/local-crm-draft-handoff.md +1 -1
- package/references/portable-ledger-handoff.md +11 -10
- package/references/recipes/catalog.json +263 -7
- package/references/recipes/command-notes.md +5 -3
- package/references/recipes/internal-link-improvement.md +25 -0
- package/references/recipes/placement-reconciliation.md +53 -1
- package/scripts/portable-handoff.py +17 -4
- package/shared/admission-contract.js +3 -2
- package/shared/admission-report-contract.js +3 -2
- package/shared/analytics-history-contract.js +15 -0
- package/shared/campaign-outcome-contract.js +12 -0
- package/shared/campaign-prospect-contract.js +23 -0
- package/shared/classification-contract.js +54 -0
- package/shared/contact-record-contract.js +27 -0
- package/shared/disavow-contract.js +37 -9
- package/shared/index-observation-contract.js +5 -4
- package/shared/internal-links-contract.js +143 -0
- package/shared/link-category-contract.js +35 -0
- package/shared/link-category.js +138 -0
- package/shared/link-labels.js +26 -0
- package/shared/link-summary-contract.js +17 -0
- package/shared/observation-event.js +1 -1
- package/shared/onboarding.js +20 -0
- package/shared/response-contract.js +38 -5
- package/shared/search-connection-contract.js +2 -1
- package/shared/table-page-contract.js +13 -0
- package/skills/agentlinkops-assets/SKILL-DEPS.json +290 -0
- package/skills/agentlinkops-assets/SKILL.md +3 -1
- package/skills/agentlinkops-assets/agents/openai.yaml +1 -1
- package/skills/agentlinkops-assets/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-assets/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-assets/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-assets/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-assets/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-assets/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-assets/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-assets/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-campaigns/SKILL-DEPS.json +336 -0
- package/skills/agentlinkops-campaigns/SKILL.md +7 -5
- package/skills/agentlinkops-campaigns/agents/openai.yaml +1 -1
- package/skills/agentlinkops-campaigns/references/first-campaign.md +31 -0
- package/skills/agentlinkops-campaigns/references/mailbox-campaign.md +46 -0
- package/skills/agentlinkops-campaigns/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-campaigns/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-campaigns/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-campaigns/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-campaigns/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-connect/SKILL-DEPS.json +104 -0
- package/skills/agentlinkops-connect/SKILL.md +2 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +1 -1
- package/skills/agentlinkops-connect/references/agentlinkops.md +164 -62
- package/skills/agentlinkops-connect/references/placement-workflows.md +19 -0
- package/skills/agentlinkops-crm/SKILL-DEPS.json +381 -0
- package/skills/agentlinkops-crm/SKILL.md +26 -9
- package/skills/agentlinkops-crm/agents/openai.yaml +1 -1
- package/skills/agentlinkops-crm/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-crm/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-crm/references/portable/references/local-crm-draft-handoff.md +76 -0
- package/skills/agentlinkops-crm/references/portable/references/portable-ledger-handoff.md +51 -0
- package/skills/agentlinkops-crm/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-crm/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-crm/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-crm/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-crm/references/portable/scripts/outreach-crm-draft.py +121 -0
- package/skills/agentlinkops-crm/references/portable/scripts/portable-handoff.py +197 -0
- package/skills/agentlinkops-crm/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-crm/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-discovery/SKILL-DEPS.json +301 -0
- package/skills/agentlinkops-discovery/SKILL.md +4 -2
- package/skills/agentlinkops-discovery/references/portable/references/cli.md +220 -0
- package/skills/agentlinkops-discovery/references/portable/references/disavow.md +73 -0
- package/skills/agentlinkops-discovery/references/portable/references/discovery-handoff.md +32 -0
- package/skills/agentlinkops-discovery/references/portable/references/reports.md +53 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/agentlinkops.py +676 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/disavow.py +207 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/001_crm.sql +45 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/002_cloud_events.sql +13 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/003_disavow.sql +20 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/migrations/004_placement_costs.sql +2 -0
- package/skills/agentlinkops-discovery/references/portable/scripts/report.py +113 -0
- package/skills/agentlinkops-discovery/references/portable/templates/campaigns.v1.json +65 -0
- package/skills/agentlinkops-discovery/references/saved-backlink-workflows.md +23 -0
- package/src/competitors/contract.js +17 -1
- package/src/context/site-profile.js +34 -121
- package/src/corpus/acquire.js +68 -32
- package/src/corpus/extract.js +147 -0
- package/src/corpus/frontier.js +151 -0
- package/src/discovery/contract.js +8 -0
- package/src/discovery/import.js +13 -1
- package/src/mentions.js +193 -0
- package/src/page-acquisition/acquire.js +50 -0
- package/src/page-acquisition/bytes.js +138 -0
- package/src/page-acquisition/capture.js +11 -0
- package/src/page-acquisition/contracts.js +67 -0
- package/src/page-acquisition/documents.js +166 -0
- package/src/page-acquisition/http.js +307 -0
- package/src/page-acquisition/policy.js +20 -0
- package/src/page-acquisition/recovery.js +164 -0
- package/src/page-acquisition/render-failure.js +82 -0
- package/src/page-extraction/html.js +359 -0
- package/src/page-extraction/index.js +30 -0
- package/src/page-extraction/metadata.js +16 -0
- package/src/response-shaping.js +4 -3
- package/src/tool-views.js +22 -12
- package/src/verifier/fetch.js +4 -339
- package/src/verifier/index.js +81 -23
- package/src/verifier/page-quality.js +14 -0
- package/src/verifier/parser.js +2 -241
- package/src/verifier/readiness.js +64 -13
- package/src/verifier/render-evidence.js +25 -2
- package/src/verifier/target.js +201 -0
- package/src/verifier/transition.js +18 -3
- package/src/verifier/url.js +31 -0
- package/src/watch-crawlability.js +144 -0
- package/skills/content-defingerprinting/SKILL.md +0 -29
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -102
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -26
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -52
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -21
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -96
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -432
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -86
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -460
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -651
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -27
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -212
- package/skills/internal-linking-optimizer/SKILL.md +0 -56
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -76
- package/skills/pre-publish-review/SKILL.md +0 -179
- package/skills/source-cited-content-builder/SKILL.md +0 -56
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -19
- package/skills/source-cited-humanizer/SKILL.md +0 -20
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
const id=z.string().min(1).max(200),digest=z.string().regex(/^sha256:[a-f0-9]{64}$/),revision=z.number().int().min(0);
|
|
3
|
+
export const classificationReadInput={watchId:id};
|
|
4
|
+
export const classificationHistoryInput={watchId:id,occurrenceId:digest,captureId:id,evidenceHash:digest,limit:z.number().int().min(1).max(50).optional(),cursor:z.string().min(1).max(2048).optional()};
|
|
5
|
+
const historyDimension=z.enum(['page_surface','link_role','region_inferred','relevance','risk_cues','acquisition']);
|
|
6
|
+
const historyState=z.enum(['accepted_manual','accepted_auto','suggested','superseded','insufficient_evidence','not_evaluated']);
|
|
7
|
+
const historyValue=z.string().min(1).max(100).nullable();
|
|
8
|
+
const historyAxis=z.object({axis:historyDimension,value:historyValue,state:historyState,basis:z.enum(['model','rule','manual','attributed'])});
|
|
9
|
+
export const classificationHistoryResponse=z.object({schemaVersion:z.literal(1),historyMeaning:z.string().min(1).max(1000),binding:z.object({workspaceId:id,projectId:id,watchId:id,occurrenceId:digest,captureId:id,evidenceHash:digest}),asOfRevision:revision,readAt:z.string().datetime(),
|
|
10
|
+
items:z.array(z.object({id:z.string().min(1).max(240),kind:z.enum(['model_receipt','correction','revocation','concern']),revision,occurredAt:z.string().datetime(),recordedAt:z.string().datetime(),actorId:id.nullable(),axis:historyDimension.nullable(),value:historyValue,state:historyState.nullable(),reason:z.string().min(1).max(500).nullable(),receiptId:id.nullable(),axes:z.array(historyAxis).min(1).max(6).optional(),policy:z.object({taxonomyVersion:id,rubricVersion:id,extractorVersion:id,privacyRevision:id}).optional()})).max(50),
|
|
11
|
+
nextCursor:z.string().min(1).max(2048).nullable(),hasMore:z.boolean(),coverage:z.object({status:z.enum(['retained','expired','unavailable']),reason:z.string().min(1).max(200),meaning:z.string().min(1).max(1000)})}).strict();
|
|
12
|
+
export const classificationPrepareInput={watchId:id,expectedRevision:revision};
|
|
13
|
+
export const classificationReviewInput={watchId:id,workspaceId:id.optional(),projectId:id.optional(),action:z.enum(['correct','revoke','concern']),occurrenceId:digest,captureId:id,evidenceHash:digest,expectedRevision:revision,
|
|
14
|
+
axis:z.enum(['page_surface','link_role','region_inferred','relevance','risk_cues','acquisition']).optional(),value:z.string().min(1).max(100).optional(),reason:z.string().trim().min(1).max(500)};
|
|
15
|
+
export const classificationAllowanceResponse=z.object({contract:z.literal('classification-allowance-v1'),unit:z.literal('classification-assessment-v1'),
|
|
16
|
+
scope:z.object({kind:z.literal('workspace'),workspaceId:id,sharedAcrossWebsites:z.literal(true)}).strict(),
|
|
17
|
+
state:z.enum(['unavailable','not_entitled','exhausted','available']),
|
|
18
|
+
limit:z.number().int().nonnegative().nullable(),reserved:z.number().int().nonnegative().nullable(),used:z.number().int().nonnegative().nullable(),remaining:z.number().int().nonnegative().nullable(),
|
|
19
|
+
period:z.object({key:id,startsAt:z.string().datetime(),endsAt:z.string().datetime()}).strict().nullable(),resetAt:z.string().datetime().nullable()}).strict();
|
|
20
|
+
export const classificationResponse=z.object({version:z.literal('placement-context-v1'),binding:z.object({workspaceId:id,projectId:id,watchId:id}),revision,
|
|
21
|
+
applicability:z.string(),reason:z.string().nullable(),summary:z.object({kind:z.string(),count:z.number().int().min(0),role:z.string().nullable()}).passthrough(),
|
|
22
|
+
occurrences:z.array(z.object({id:digest,captureId:id,revision,evidenceHash:digest,axes:z.record(z.string(),z.object({value:z.string().nullable(),state:z.string()}).passthrough()),passages:z.array(z.object({id:z.string(),text:z.string()})),canCorrect:z.boolean()}).passthrough()),
|
|
23
|
+
capability:z.object({suggestionsAvailable:z.boolean(),reason:z.string().nullable(),classificationAllowance:classificationAllowanceResponse.optional()})}).passthrough();
|
|
24
|
+
export const classificationQuoteInput={watchId:id,occurrenceId:digest,captureId:id,evidenceHash:digest,expectedRevision:revision,model:z.enum(['clef','clef-flash']).optional()};
|
|
25
|
+
export const classificationStartInput={watchId:id,quoteId:id,idempotencyKey:z.string().min(8).max(200)};
|
|
26
|
+
export const classificationJobInput={watchId:id,jobId:id};
|
|
27
|
+
export const classificationReconcileInput={...classificationJobInput,expectedRevision:revision};
|
|
28
|
+
export const classificationQuoteResponse=z.object({id,maxCalls:z.literal(1),maxInputTokens:z.literal(65536),ceilingMicrousd:z.number().int().positive(),expiresAt:z.string().datetime(),ready:z.boolean(),reason:z.string().nullable().optional()}).passthrough();
|
|
29
|
+
export const classificationJobResponse=z.object({id,state:z.enum(['queued','dispatched','succeeded','failed','reconciliation_required','cancelled']),reason:z.string().nullable().optional()}).passthrough();
|
|
30
|
+
|
|
31
|
+
const metadataFamily=z.enum(['organizational','subtype','semantic']);
|
|
32
|
+
const metadataDefinition=z.object({id,family:metadataFamily,name:z.string().trim().min(1).max(64),color:z.string().regex(/^#[a-fA-F0-9]{6}$/).nullable().optional(),order:z.number().int().min(0).max(1000).optional(),retired:z.boolean().optional(),parentId:id.optional(),taxonomyVersion:id.optional(),definition:z.string().min(1).max(1000).optional(),inclusions:z.array(z.string().min(1).max(500)).min(1).max(8).optional(),exclusions:z.array(z.string().min(1).max(500)).min(1).max(8).optional(),positiveExamples:z.array(z.string().min(1).max(500)).min(1).max(8).optional(),negativeExamples:z.array(z.string().min(1).max(500)).min(1).max(8).optional(),insufficientEvidence:z.string().min(1).max(500).optional()}).strict();
|
|
33
|
+
const metadataSettings=z.object({taxonomyVersion:id.optional(),roleAliases:z.record(z.string(),z.string().min(1).max(64)).optional(),roleGroups:z.record(z.string(),z.string().min(1).max(64)).optional(),hiddenRoleIds:z.array(id).max(20).optional(),roleOrder:z.array(id).max(20).optional(),suggestionsPaused:z.boolean().optional(),automaticLabels:z.literal(false).optional(),renderedImages:z.literal(false).optional(),semanticAlerts:z.literal(false).optional(),feedbackReuse:z.literal(false).optional()}).strict();
|
|
34
|
+
const metadataProject={projectId:id};
|
|
35
|
+
const metadataWrite={...metadataProject,expectedRevision:revision,reason:z.string().trim().min(1).max(500)};
|
|
36
|
+
const metadataOccurrence=z.object({watchId:id,occurrenceId:digest,captureId:id,evidenceHash:digest}).strict();
|
|
37
|
+
export const classificationMetadataGetInput={...metadataProject,asOfRevision:revision.optional()};
|
|
38
|
+
export const classificationMetadataDefinitionInput={...metadataWrite,definition:metadataDefinition};
|
|
39
|
+
export const classificationMetadataSettingsInput={...metadataWrite,level:z.enum(['project','workspace']),expectedSettingsRevision:revision,settings:metadataSettings};
|
|
40
|
+
export const classificationMetadataAttachInput={...metadataWrite,definitionId:id,action:z.enum(['attach','detach']),occurrences:z.array(metadataOccurrence).min(1).max(100)};
|
|
41
|
+
export const classificationMetadataOccurrenceInput={...metadataProject,...metadataOccurrence.shape,asOfRevision:revision.optional()};
|
|
42
|
+
export const classificationMetadataResponse=z.object({binding:z.object({workspaceId:id,projectId:id}),revision,asOfRevision:revision,definitions:z.array(metadataDefinition.passthrough()).max(70),settings:metadataSettings,origins:z.record(z.string(),z.enum(['default','workspace','project'])),workspaceRevision:revision,projectRevision:revision,inferenceEffect:z.literal('none'),enrollmentEffect:z.literal('none'),readAt:z.string().datetime()}).passthrough();
|
|
43
|
+
export const classificationMetadataWriteResponse=z.object({revision,inferenceEffect:z.literal('none')}).passthrough();
|
|
44
|
+
export const classificationMetadataOccurrenceResponse=z.object({binding:z.object({workspaceId:id,projectId:id,watchId:id,occurrenceId:digest,captureId:id}).passthrough(),revision,asOfRevision:revision,items:z.array(z.object({definition:metadataDefinition.passthrough(),semanticRevision:revision,actorId:id,recordedAt:z.string().datetime(),retired:z.boolean(),applicable:z.boolean(),state:z.enum(['accepted_manual','customer_metadata'])}).passthrough()).max(21),inferenceEffect:z.literal('none')}).passthrough();
|
|
45
|
+
|
|
46
|
+
export const classificationOutputs={get_classification_metadata:classificationMetadataResponse,save_classification_definition:classificationMetadataWriteResponse,save_classification_settings:classificationMetadataWriteResponse,tag_placement_occurrences:classificationMetadataWriteResponse,get_placement_metadata:classificationMetadataOccurrenceResponse,get_placement_context:classificationResponse,get_placement_review_history:classificationHistoryResponse,prepare_placement_context:classificationResponse,review_placement_context:classificationResponse,
|
|
47
|
+
quote_placement_classification:classificationQuoteResponse,classify_placement:classificationJobResponse,get_placement_classification_job:classificationJobResponse,reconcile_placement_classification:classificationJobResponse};
|
|
48
|
+
|
|
49
|
+
// Illustrative documentation data. Never used as runtime fallback.
|
|
50
|
+
const emptyContext={version:'placement-context-v1',binding:{workspaceId:'workspace_example',projectId:'project_example',watchId:'watch_example'},revision:0,applicability:'unavailable',reason:'no_retained_classification',summary:{kind:'unclassified',count:0,role:null},occurrences:[],capability:{suggestionsAvailable:false,reason:'provider_admission_required'}};
|
|
51
|
+
const savedJob={id:'classification_job_example',state:'succeeded',reason:null,canReconcile:false};
|
|
52
|
+
export const classificationExamples={get_classification_metadata:{binding:{workspaceId:"workspace_example",projectId:"project_example"},revision:0,asOfRevision:0,definitions:[],settings:{suggestionsPaused:true},origins:{suggestionsPaused:"default"},workspaceRevision:0,projectRevision:0,inferenceEffect:"none",enrollmentEffect:"none",readAt:"2026-10-09T20:00:00.000Z"},save_classification_definition:{revision:1,inferenceEffect:"none"},save_classification_settings:{revision:1,inferenceEffect:"none"},tag_placement_occurrences:{revision:1,inferenceEffect:"none"},get_placement_metadata:{binding:{workspaceId:"workspace_example",projectId:"project_example",watchId:"watch_example",occurrenceId:"sha256:"+"1".repeat(64),captureId:"capture_example"},revision:0,asOfRevision:0,items:[],inferenceEffect:"none"},get_placement_context:emptyContext,get_placement_review_history:{schemaVersion:1,historyMeaning:'Only retained reviews for this exact occurrence and capture are included.',binding:{...emptyContext.binding,occurrenceId:'sha256:'+'1'.repeat(64),captureId:'capture_example',evidenceHash:'sha256:'+'2'.repeat(64)},asOfRevision:1,readAt:'2026-10-08T20:00:00.000Z',items:[],nextCursor:null,hasMore:false,coverage:{status:'retained',reason:'exact_capture_only',meaning:'Missing history does not establish that no earlier changes occurred.'}},prepare_placement_context:emptyContext,review_placement_context:emptyContext,
|
|
53
|
+
quote_placement_classification:{id:'classification_quote_example',maxCalls:1,maxInputTokens:65536,ceilingMicrousd:15729,expiresAt:'2026-10-08T20:10:00.000Z',ready:true},
|
|
54
|
+
classify_placement:{id:'classification_job_example',state:'queued',reason:null,canReconcile:false},get_placement_classification_job:savedJob,reconcile_placement_classification:savedJob};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
|
|
3
|
+
const id=z.string().min(1).max(200);
|
|
4
|
+
const text=max=>z.string().trim().min(1).max(max).refine(v=>!/[\u0000-\u001f\u007f]/.test(v),'Control characters are not allowed');
|
|
5
|
+
// External references are opaque record keys, never contact methods or credentials.
|
|
6
|
+
export const contactExternalRef=text(200).refine(v=>!/@|https?:\/\//i.test(v),'Use an opaque source record reference');
|
|
7
|
+
const contactUrl=z.url().max(2000).refine(value=>{const u=new URL(value);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password;},'Use an HTTP contact page without credentials');
|
|
8
|
+
export const contactFields=z.object({externalRef:contactExternalRef,name:text(120),email:z.email().max(254).nullable(),contactUrl:contactUrl.nullable()}).strict();
|
|
9
|
+
const recordRevision=z.uuid().describe('Revision of this saved contact. Supply it as revision when updating or deleting the contact.');
|
|
10
|
+
const expectedRevision=z.union([z.literal(0),recordRevision]).describe('Current contact revision from list_contacts or save_contact; 0 creates.');
|
|
11
|
+
export const contactRecord=contactFields.extend({id,projectId:id,revision:recordRevision,createdAt:z.string(),updatedAt:z.string(),attention:z.unknown().nullable().optional(),relatedRecords:z.array(z.object({watchId:id,sourceUrl:z.string().nullable(),publisherUrl:z.string(),targetUrl:z.string(),progress:z.string().nullable(),monitoring:z.boolean(),participationId:id.optional(),campaign:z.string().optional(),lastContactedAt:z.string().nullable().optional()}).strict()).optional(),hasMoreRelatedRecords:z.boolean().optional()}).strict();
|
|
12
|
+
export const CONTACT_TERMS_VERSION='publisher-contacts-v1';
|
|
13
|
+
export const CONTACT_TERMS_URL='https://agentlinkops.com/terms/#your-data';
|
|
14
|
+
export const saveContactInput=z.object({projectId:id,revision:expectedRevision,contact:contactFields,acceptTermsVersion:z.string().min(1).max(80).optional().describe('Supply the current terms version only after an owner or administrator explicitly agrees for everyone saving contacts on this website. Never infer agreement from an import or contact read.')}).strict();
|
|
15
|
+
export const saveContactOutput=z.object({contact:contactRecord,replayed:z.boolean()}).strict();
|
|
16
|
+
// An opaque association revision prevents a stale editor from matching a new association
|
|
17
|
+
// after a contact deletion cascades the old row. Zero means no association exists yet.
|
|
18
|
+
const linkRevisionDescription='Current association revision from list_contacts; 0 creates the first association.';
|
|
19
|
+
const linkRevision=z.union([z.literal(0).describe(linkRevisionDescription),z.uuid().describe(linkRevisionDescription)]).describe(linkRevisionDescription);
|
|
20
|
+
export const contactLink=z.object({watchId:id,contactId:id.nullable(),revision:linkRevision}).strict();
|
|
21
|
+
export const listContactsInput=z.object({projectId:id,externalRef:contactExternalRef.optional(),contactId:id.optional(),watchId:id.optional(),after:id.optional(),limit:z.number().int().min(1).max(100).default(50)}).strict();
|
|
22
|
+
export const contactTermsState=z.object({version:z.string(),accepted:z.boolean(),canAcknowledge:z.boolean(),ready:z.boolean(),url:z.literal(CONTACT_TERMS_URL)}).strict();
|
|
23
|
+
export const listContactsOutput=z.object({items:z.array(contactRecord),nextAfter:id.nullable(),link:contactLink.nullable(),terms:contactTermsState.optional()}).strict();
|
|
24
|
+
export const deleteContactInput=z.object({projectId:id,contactId:id,revision:recordRevision,onlyIfUnlinked:z.boolean().default(false)}).strict();
|
|
25
|
+
export const deleteContactOutput=z.object({contactId:id,deleted:z.boolean()}).strict();
|
|
26
|
+
export const setLinkContactInput=z.object({projectId:id,watchId:id,contactId:id.nullable(),revision:linkRevision}).strict();
|
|
27
|
+
export const contactResponses={list_contacts:listContactsOutput,save_contact:saveContactOutput,delete_contact:deleteContactOutput,set_link_contact:contactLink};
|
|
@@ -1,18 +1,46 @@
|
|
|
1
|
+
import {browsePageInput,browsePageOutput,browseCursor} from './table-page-contract.js';
|
|
1
2
|
import {z} from 'zod';
|
|
2
|
-
export const DISAVOW_LIMITS=Object.freeze({projectRules:
|
|
3
|
+
export const DISAVOW_LIMITS=Object.freeze({projectRules:10000,ruleBytes:3072,importRules:500,importBytes:240000,comments:8,commentChars:120});
|
|
3
4
|
const id=z.string().min(1).max(200),revision=z.number().int().positive();
|
|
4
5
|
const comments=z.array(z.string().max(120).startsWith('#')).max(8);
|
|
5
|
-
export const disavowListInput=z.object({projectId:id,status:z.enum(['proposed','active','rejected']).optional(),limit:z.number().int().min(1).max(100).optional(),cursor:
|
|
6
|
-
export const disavowHistoryInput=z.object({projectId:id,limit:z.number().int().min(1).max(100).optional(),cursor:
|
|
6
|
+
export const disavowListInput=z.object({...browsePageInput,projectId:id.optional(),status:z.enum(['proposed','active','rejected']).optional(),limit:z.number().int().min(1).max(100).optional(),cursor:browseCursor.optional()}).strict();
|
|
7
|
+
export const disavowHistoryInput=z.object({...browsePageInput,projectId:id,limit:z.number().int().min(1).max(100).optional(),cursor:browseCursor.optional()}).strict();
|
|
7
8
|
export const disavowProposeInput=z.object({projectId:id,kind:z.enum(['domain','url']),value:z.string().min(1).max(2048),comments:comments.optional(),source:z.enum(['manual','agent_proposal']).optional(),status:z.enum(['proposed','active']).optional()}).strict();
|
|
8
|
-
export const disavowImportInput=z.object({projectId:id,text:z.string().max(
|
|
9
|
+
export const disavowImportInput=z.object({projectId:id,text:z.string().max(240000),approve:z.boolean().optional()}).strict();
|
|
10
|
+
export const disavowReviewManyInput=z.object({projectId:id,rules:z.array(z.object({ruleId:id,revision}).strict()).min(1).max(100),decision:z.enum(['approve','reject'])}).strict();
|
|
9
11
|
export const disavowReviewInput=z.object({projectId:id,ruleId:id,revision,decision:z.enum(['approve','reject'])}).strict();
|
|
10
12
|
export const disavowDeleteInput=z.object({projectId:id,ruleId:id,revision}).strict();
|
|
11
13
|
export const disavowProjectInput=z.object({projectId:id}).strict();
|
|
12
|
-
export const disavowRule=z.object({id,workspace_id:id,project_id:id,kind:z.enum(['domain','url']),value:z.string(),identity:z.string(),source:z.enum(['manual','import','agent_proposal']),status:z.enum(['proposed','active','rejected']),comments,revision,created_by:id,approved_by:id.nullable(),created_at:z.string(),updated_at:z.string()}).strict();
|
|
13
|
-
|
|
14
|
-
export const
|
|
14
|
+
export const disavowRule=z.object({id,workspace_id:id,project_id:id,kind:z.enum(['domain','url']),value:z.string(),identity:z.string(),source:z.enum(['manual','import','agent_proposal']),status:z.enum(['proposed','active','rejected']),comments,revision,created_by:id,approved_by:id.nullable(),created_at:z.string(),updated_at:z.string(),covered_links:z.number().int().nonnegative().optional(),created_by_label:z.enum(['you','imported','agent','owner','admin','member','viewer','former member']).optional()}).strict();
|
|
15
|
+
const count=z.number().int().nonnegative();
|
|
16
|
+
export const disavowSummary=z.object({active:count,proposed:count,rejected:count}).strict();
|
|
17
|
+
export const disavowListOutput=z.object({...browsePageOutput,rules:z.array(disavowRule),revision:count,cursor:z.string().nullable(),summary:disavowSummary.optional()}).strict();
|
|
18
|
+
export const disavowMutationOutput=z.object({rules:z.array(disavowRule),created:count,unchanged:count,revision:count,approved:z.boolean().optional(),counts:z.object({sites:count,pages:count,already_listed:count}).strict().optional(),warnings:z.array(z.string()).max(50).optional(),failed:z.array(z.object({ruleId:id,code:z.string()}).strict()).optional()}).strict();
|
|
15
19
|
export const disavowDeleteOutput=z.object({deleted:z.literal(true),rule_id:id,revision:z.number().int().nonnegative()}).strict();
|
|
16
|
-
export const disavowExportReceipt=z.object({id,workspace_id:id,project_id:id,format_version:z.literal(1),sha256:z.string().regex(/^[a-f0-9]{64}$/),stored_active_count:z.number().int().nonnegative(),effective_count:z.number().int().nonnegative(),byte_length:z.number().int().nonnegative(),collection_revision:z.number().int().nonnegative(),source_revisions:z.array(z.object({id,revision}).strict()),created_by:id,created_at:z.string()}).strict();
|
|
20
|
+
export const disavowExportReceipt=z.object({id,workspace_id:id,project_id:id,format_version:z.literal(1),sha256:z.string().regex(/^[a-f0-9]{64}$/),stored_active_count:z.number().int().nonnegative(),effective_count:z.number().int().nonnegative(),byte_length:z.number().int().nonnegative(),collection_revision:z.number().int().nonnegative(),source_revisions:z.array(z.object({id,revision}).strict()),created_by:id,created_at:z.string(),exported_by_label:z.enum(['you','owner','admin','member','viewer','former member']).optional()}).strict();
|
|
17
21
|
export const disavowExportOutput=z.object({filename:z.literal('disavow.txt'),content_type:z.literal('text/plain; charset=utf-8'),text:z.string(),receipt:disavowExportReceipt}).strict();
|
|
18
|
-
export const disavowHistoryOutput=z.object({receipts:z.array(disavowExportReceipt),cursor:z.string().nullable()}).strict();
|
|
22
|
+
export const disavowHistoryOutput=z.object({...browsePageOutput,receipts:z.array(disavowExportReceipt),cursor:z.string().nullable(),changed_since:count.nullable().optional(),revision:count.optional()}).strict();
|
|
23
|
+
// Agent handoffs: what Google holds, checked against the approved list (never assumed).
|
|
24
|
+
export const DISAVOW_CHECK_LIMITS=Object.freeze({textBytes:2000000,examples:20,counts:100000});
|
|
25
|
+
const day=z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
26
|
+
export const handoffInput=z.object({projectId:id,handoff:z.enum(['disavow_upload'])}).strict();
|
|
27
|
+
export const disavowConfirmInput=z.object({projectId:id,text:z.string().max(DISAVOW_CHECK_LIMITS.textBytes).optional(),counts:z.object({domains:z.number().int().min(0).max(DISAVOW_CHECK_LIMITS.counts),urls:z.number().int().min(0).max(DISAVOW_CHECK_LIMITS.counts),uploaded_on:day.optional()}).strict().optional()}).strict();
|
|
28
|
+
const examples=z.array(z.string()).max(DISAVOW_CHECK_LIMITS.examples);
|
|
29
|
+
export const disavowGoogleCheck=z.object({id,checked_at:z.string(),method:z.enum(['text','counts']),result:z.enum(['match','differs','counts_match','counts_differ']),missing_in_google:count,extra_in_google:count,examples:z.object({missing_in_google:examples,extra_in_google:examples}).strict(),google:z.object({domains:count,urls:count}).strict(),uploaded_on:day.nullable(),checked_by:z.enum(['person','agent'])}).strict();
|
|
30
|
+
export const disavowGoogleState=z.enum(['never_checked','in_sync','behind','differs','counts_only']);
|
|
31
|
+
export const disavowStatusOutput=z.object({
|
|
32
|
+
project_id:id,
|
|
33
|
+
rules:z.object({active:count,effective:count,domains:count,urls:count}).strict(),
|
|
34
|
+
changed_since_download:count.nullable(),
|
|
35
|
+
last_change_at:z.string().nullable(),
|
|
36
|
+
last_download:disavowExportReceipt.nullable(),
|
|
37
|
+
google:z.object({state:disavowGoogleState,changes_since_check:count.nullable(),last_check:disavowGoogleCheck.nullable()}).strict(),
|
|
38
|
+
}).strict();
|
|
39
|
+
export const disavowConfirmOutput=z.object({check:disavowGoogleCheck,status:disavowStatusOutput}).strict();
|
|
40
|
+
const handoffStep=z.object({text:z.string(),href:z.string().url().optional(),action:z.enum(['download','verify']).optional()}).strict();
|
|
41
|
+
export const handoffOutput=z.object({
|
|
42
|
+
id:z.enum(['disavow_upload']),title:z.string(),why:z.string(),prompt:z.string(),steps:z.array(handoffStep),
|
|
43
|
+
agent:z.object({goal:z.string(),needs:z.array(z.string()),procedure:z.array(z.string()),verify:z.object({command:z.string(),input:z.string()}).strict(),stop_if:z.array(z.string()),never:z.array(z.string())}).strict(),
|
|
44
|
+
website:z.object({project_id:id,domain:z.string()}).strict(),
|
|
45
|
+
status:disavowStatusOutput,
|
|
46
|
+
}).strict();
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import {browsePageInput,browsePageOutput,browseCursor} from './table-page-contract.js';
|
|
1
2
|
import {z} from 'zod';
|
|
2
3
|
const id=z.string().min(1).max(256),digest=z.string().regex(/^[a-f0-9]{64}$/),time=z.string();
|
|
3
4
|
export const indexConfigureInput=z.object({projectId:id,enabled:z.boolean(),dailyLimit:z.number().int().min(1).max(2000).optional(),retentionDays:z.number().int().min(1).max(90).optional(),cadenceSeconds:z.number().int().min(3600).max(2592000).optional()}).strict();
|
|
4
5
|
export const indexInspectInput=z.object({connectionId:id,watchId:id,idempotencyKey:z.string().regex(/^[\x21-\x7e]{1,128}$/)}).strict();
|
|
5
|
-
export const indexHistoryInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
6
|
+
export const indexHistoryInput=z.object({...browsePageInput,cursor:browseCursor.optional(),projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
6
7
|
export const indexReceiptInput=z.object({projectId:id,watchId:id,receiptId:digest}).strict();
|
|
7
|
-
export const indexEventsInput=z.object({projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
8
|
+
export const indexEventsInput=z.object({...browsePageInput,cursor:browseCursor.optional(),projectId:id,watchId:id,limit:z.number().int().min(1).max(100).optional(),beforeId:digest.optional()}).strict();
|
|
8
9
|
export const indexSettingsInput=z.object({projectId:id}).strict();
|
|
9
10
|
export const indexSettingsResponse=z.object({projectId:id,enabled:z.boolean(),dailyLimit:z.number().int().positive(),retentionDays:z.number().int().positive(),cadenceSeconds:z.number().int().positive(),schedulingAvailable:z.boolean()}).passthrough();
|
|
10
11
|
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)});
|
|
@@ -12,5 +13,5 @@ export const indexReceiptResponse=z.object({observation:indexObservation,rawResp
|
|
|
12
13
|
export const indexInspectResponse=indexReceiptResponse.extend({replayed:z.boolean(),providerError:z.enum(['PROVIDER_PERMISSION_LOST','PROVIDER_RATE_LIMITED','PROVIDER_UNAVAILABLE','PROVIDER_RESPONSE_INVALID']).nullable().optional()});
|
|
13
14
|
export const indexAttemptSummary=z.object({observedAt:time,latestAttempt:z.object({id:digest,admittedAt:time,status:z.enum(['in_progress','unsettled','completed']),receiptId:digest.nullable(),leaseExpiresAt:time.nullable()}).nullable(),unsettledCount:z.number().int().nonnegative()});
|
|
14
15
|
export const indexWebsiteQuota=z.object({windowStart:time,observedAt:time,admitted:z.number().int().nonnegative(),limit:z.number().int().positive(),remaining:z.number().int().nonnegative()});
|
|
15
|
-
export const indexHistoryResponse=z.object({observations:z.array(indexObservation),nextCursor:
|
|
16
|
-
export const indexEventsResponse=z.object({events:z.array(z.looseObject({id:digest,receiptId:digest,type:z.enum(['index.deindexed','index.reindexed']),createdAt:time,url:z.url(),source_key:z.string(),before:z.enum(['indexed','not_indexed']),after:z.enum(['indexed','not_indexed']),observation_ids:z.array(id)})),nextCursor:
|
|
16
|
+
export const indexHistoryResponse=z.object({observations:z.array(indexObservation),nextCursor:browseCursor.nullable(),attemptSummary:indexAttemptSummary.optional(),websiteQuota:indexWebsiteQuota.optional()}).passthrough();
|
|
17
|
+
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:browseCursor.nullable().optional(),hasMore:z.boolean().optional()}).passthrough();
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Shared UI/agent limits. These are research captures, not recurring monitor enrollment.
|
|
2
|
+
export const INTERNAL_LINK_VERSION = 'internal-links-v1';
|
|
3
|
+
export const INTERNAL_LINK_LIMITS = Object.freeze({inventoryPages:100,pagesPerRequest:3,pageBytes:2*1024*1024,
|
|
4
|
+
pageTimeoutMs:20_000,requestTimeoutMs:65_000,edgesPerPage:1000,evidenceBytesPerPage:1024*1024,workspacePagesPerDay:300,
|
|
5
|
+
retainedSnapshotsPerProject:20,retentionDays:30,pageSize:100,leaseMs:90_000});
|
|
6
|
+
export const INTERNAL_LINK_SOURCES = Object.freeze(['sitemap_discovery','url_list','sitemap_export','cms_export','repository_export','search_export']);
|
|
7
|
+
export const INTERNAL_LINK_VIEWS = Object.freeze(['pages','edges','findings','diff','summary','graph','actions']);
|
|
8
|
+
export const INTERNAL_LINK_SCAN_VIEWS = Object.freeze(['pages','edges','findings','summary','graph','actions','opportunities','changes','map','inventory']);
|
|
9
|
+
export const INTERNAL_LINK_SCAN_SORTS = Object.freeze(['url','incoming','outgoing','score','impressions']);
|
|
10
|
+
export const INTERNAL_LINK_PAGE_HEALTH_BUCKETS = Object.freeze(['no_observed_issues','needs_review','no_incoming','broken_outgoing','unknown','pending','excluded','unassessed']);
|
|
11
|
+
export const INTERNAL_LINK_SCAN_CADENCES = Object.freeze(['off','weekly','monthly']);
|
|
12
|
+
export const INTERNAL_LINK_REGIONS = Object.freeze(['main','navigation','footer','header','aside','unknown']);
|
|
13
|
+
|
|
14
|
+
import {z} from 'zod';
|
|
15
|
+
const id=z.string().min(1).max(200),n=z.number().int().nonnegative(),time=z.string();
|
|
16
|
+
export const internalLinkPageHealthSummary=z.strictObject({version:z.literal('observed-page-health-v1'),readiness:z.enum(['ready','not_ready']),
|
|
17
|
+
scope:z.strictObject({kind:z.literal('saved_scan_observations'),completeSiteCensus:z.literal(false),incomingBasis:z.literal('distinct_other_pages_all_link_regions'),
|
|
18
|
+
eligibleBasis:z.literal('complete_indexable_html_at_own_canonical_address'),frontierTruncated:z.boolean().nullable(),allInventoryPagesComplete:z.boolean().nullable()}),
|
|
19
|
+
pages:n.nullable(),assessedPages:n.nullable(),counts:z.strictObject(Object.fromEntries(INTERNAL_LINK_PAGE_HEALTH_BUCKETS.map(key=>[key,n]))).nullable(),
|
|
20
|
+
reasons:z.record(z.string(),n).nullable(),brokenOutgoingPairs:n.nullable()});
|
|
21
|
+
const limits=z.strictObject(Object.fromEntries(Object.entries(INTERNAL_LINK_LIMITS).map(([key,value])=>[key,z.literal(value)])));
|
|
22
|
+
const snapshotCoverage=z.strictObject({inventory:z.enum(['declared_complete','partial']),representation:z.literal('raw_html'),completeSiteCensus:z.literal(false),allInventoryPagesComplete:z.boolean(),renderedVisibility:z.literal(false),iframes:z.literal(false),shadowRoots:z.literal(false),canonicalAliasesMerged:z.literal(false)});
|
|
23
|
+
export const INTERNAL_LINK_DISCOVERY_LIMITS=Object.freeze({sitemapDocuments:3,indexDepth:2,timeoutMs:20_000,documentBytes:2*1024*1024,offeredUrls:100,inventoryUrlBytes:128*1024,requests:10,retentionMs:15*60_000,startsPerMinute:3});
|
|
24
|
+
const discoveryLimits=z.strictObject(Object.fromEntries(Object.entries(INTERNAL_LINK_DISCOVERY_LIMITS).map(([key,value])=>[key,z.literal(value)])));
|
|
25
|
+
const discoveryDocument=z.strictObject({requestedUrl:z.string(),finalUrl:z.string().nullable(),checkedAt:time,sha256:z.string().nullable(),status:n.nullable(),reason:z.string().nullable(),kind:z.enum(['robots','index','urlset','unknown'])});
|
|
26
|
+
const discoveryCoverage=z.strictObject({completeSiteCensus:z.literal(false),truncated:z.boolean(),reasons:z.array(z.string())});
|
|
27
|
+
export const internalLinkInventoryProvenance=z.strictObject({previewId:id,discoveredAt:time,domain:z.string(),documents:z.array(discoveryDocument).max(13),coverage:discoveryCoverage,selectedUrls:z.array(z.string()).max(100)});
|
|
28
|
+
export const internalLinkPreviewOutput=z.strictObject({preview:z.strictObject({id,projectId:id,domain:z.string(),createdAt:time,expiresAt:time,state:z.enum(['pending','complete']),urls:z.array(z.string()).max(100),documents:z.array(discoveryDocument).max(13),coverage:discoveryCoverage}),limits:discoveryLimits});
|
|
29
|
+
export const internalLinkSnapshot=z.strictObject({id,projectId:id,createdAt:time,expiresAt:time,completedAt:time.nullable(),
|
|
30
|
+
inventoryProvenance:internalLinkInventoryProvenance.optional(),state:z.enum(['pending','running','complete']),inventoryCount:n,processedCount:n,completeCount:n,unknownCount:n,pendingCount:n,edgeCount:n,revision:n,
|
|
31
|
+
busy:z.boolean(),retryAfterSeconds:n,scope:z.strictObject({domain:z.string(),hostPolicy:z.literal('exact_hostname'),seeds:z.array(z.string())}),coverage:snapshotCoverage,version:z.literal(INTERNAL_LINK_VERSION)});
|
|
32
|
+
const capture=z.strictObject({version:z.literal(1),representation:z.enum(['raw_html','rendered_dom','document']),requestedUrl:z.string(),finalUrl:z.string(),httpStatus:n,acquiredAt:time,
|
|
33
|
+
body:z.strictObject({complete:z.boolean(),truncated:z.boolean(),bytes:n,sha256:z.string(),decodedSha256:z.string(),charset:z.string().nullable().optional()}),
|
|
34
|
+
transport:z.strictObject({id:z.string(),version:z.string(),qualified:z.boolean()}),coverage:z.strictObject({mainDocument:z.literal(true),iframes:z.boolean(),shadowRoots:z.boolean()})});
|
|
35
|
+
const pageCoverage=z.strictObject({representation:z.enum(['raw_html','rendered_dom']),documentComplete:z.boolean(),linksObserved:n,linksRetained:n,renderedVisibility:z.boolean(),iframes:z.literal(false),shadowRoots:z.literal(false),canonicalAliasesMerged:z.literal(false)});
|
|
36
|
+
export const internalLinkPage=z.strictObject({url:z.string(),source:z.enum(INTERNAL_LINK_SOURCES),important:z.boolean(),excluded:z.boolean(),state:z.enum(['pending','complete','unknown']),reason:z.string().nullable(),checkedAt:time.nullable(),finalUrl:z.string().nullable(),httpStatus:n.nullable(),
|
|
37
|
+
title:z.string().nullable().optional(),canonicalUrl:z.string().nullable().optional(),noindex:z.boolean().optional(),nofollow:z.boolean().optional(),
|
|
38
|
+
capture:capture.optional(),redirects:z.array(z.strictObject({from:z.string(),to:z.string(),status:n})).optional(),extractorVersion:z.literal(INTERNAL_LINK_VERSION).optional(),coverage:pageCoverage.optional(),
|
|
39
|
+
incomingContextual:n,outgoingContextual:n,depth:n.nullable()});
|
|
40
|
+
export const internalLinkEdge=z.strictObject({id:z.string(),sourceUrl:z.string(),sourceDocumentUrl:z.string(),targetUrl:z.string(),targetHref:z.string(),anchor:z.string(),passage:z.string(),region:z.enum(INTERNAL_LINK_REGIONS),contextual:z.boolean(),regionEvidence:z.literal('html_landmark'),rel:z.array(z.string()),locator:z.strictObject({line:n,column:n,offset:n}).nullable(),checkedAt:time,captureSha256:z.string()});
|
|
41
|
+
const findingEvidence=z.strictObject({checkedAt:time.nullable(),captureSha256:z.string().nullable(),inventorySource:z.enum(INTERNAL_LINK_SOURCES),inventoryCompleteDeclared:z.boolean(),allInventoryPagesComplete:z.boolean(),reason:z.string().nullable().optional(),incomingContextual:n.optional(),outgoingContextual:n.optional(),importanceSource:z.literal('user').optional(),finalUrl:z.string().optional(),seeds:z.array(z.string()).optional(),targetUrl:z.string().optional(),targetCheckedAt:time.nullable().optional(),httpStatus:n.optional()});
|
|
42
|
+
export const internalLinkFinding=z.strictObject({id:z.string(),kind:z.enum(['coverage_unknown','redirected_page','orphan_candidate','no_contextual_outgoing','underlinked_priority','unreachable_from_seeds','broken_destination']),pageUrl:z.string(),priority:z.enum(['high','medium','low']),message:z.string(),evidence:findingEvidence});
|
|
43
|
+
export const internalLinkDifference=z.strictObject({kind:z.enum(['added','newly_observed','no_longer_observed','unknown']),edge:internalLinkEdge,comparison:z.literal('snapshot_observation'),reason:z.string().nullable().optional(),confirmedLoss:z.literal(false).optional()});
|
|
44
|
+
const healthCoverage=z.strictObject({inventory:z.enum(['declared_complete','partial']),allInventoryPagesComplete:z.boolean(),orphanStatus:z.literal('candidates_only'),depthScope:z.literal('explicit_seeds_and_supplied_inventory'),representation:z.literal('raw_html'),classification:z.literal('html_landmark'),semanticGroups:z.literal('not_assessed'),renderedVisibility:z.literal(false)});
|
|
45
|
+
export const internalLinkSnapshotOutput=z.strictObject({snapshot:internalLinkSnapshot,limits,retryAt:time.nullable().optional()});
|
|
46
|
+
export const internalLinkListOutput=z.strictObject({items:z.array(internalLinkSnapshot).max(100),total:n,nextCursor:z.string().nullable(),limits});
|
|
47
|
+
export const internalLinkSummary=z.strictObject({coverage:z.strictObject({pages:n,complete:n,unknown:n,pending:n,edges:n}),
|
|
48
|
+
unknownByReason:z.array(z.strictObject({reason:z.string().nullable(),count:n})).max(50),
|
|
49
|
+
findingsByKind:z.array(z.strictObject({kind:z.string(),priority:z.enum(['high','medium','low']),count:n})).max(50),
|
|
50
|
+
findingsTotal:n,findingsReady:z.boolean()});
|
|
51
|
+
export const internalLinkCluster=z.strictObject({segment:z.string(),pages:n,complete:n,unknown:n,pending:n,
|
|
52
|
+
broken:n,orphanCandidates:n,topPage:z.strictObject({url:z.string(),title:z.string().nullable(),incomingContextual:n}).nullable()});
|
|
53
|
+
export const internalLinkNeighborhood=z.strictObject({pageUrl:z.string(),incomingTotal:n,outgoingTotal:n,
|
|
54
|
+
incoming:z.array(z.string()).max(100),outgoing:z.array(z.string()).max(100),contextualOnly:z.boolean()});
|
|
55
|
+
export const internalLinkAction=z.strictObject({id:z.string(),kind:z.enum(['retarget_broken','retarget_redirected','add_contextual_candidate','add_link_at_mention']),
|
|
56
|
+
priority:z.enum(['high','medium','low']),targetUrl:z.string(),sourceUrl:z.string().nullable(),anchor:z.string().nullable(),
|
|
57
|
+
passage:z.string().nullable(),message:z.string(),evidence:z.strictObject({findingKind:z.string(),httpStatus:n.optional(),
|
|
58
|
+
finalUrl:z.string().optional(),checkedAt:time.nullable().optional(),signals:z.array(z.string()).max(10),score:n.optional(),
|
|
59
|
+
// Mention opportunities: where the words came from and their place in the passage.
|
|
60
|
+
phraseSource:z.enum(['search_query','title','address']).optional(),phrase:z.string().optional(),matchStart:n.optional(),matchEnd:n.optional()})});
|
|
61
|
+
export const internalLinkQueryOutput=z.strictObject({snapshot:internalLinkSnapshot,limits,items:z.array(z.union([internalLinkPage,internalLinkEdge,internalLinkFinding,internalLinkDifference,internalLinkSummary,internalLinkCluster,internalLinkNeighborhood,internalLinkAction])).max(100),total:n,nextCursor:z.string().nullable(),coverage:z.union([snapshotCoverage,healthCoverage]),baselineSnapshot:internalLinkSnapshot.optional()});
|
|
62
|
+
|
|
63
|
+
// Initial scan-v1 admission ceiling, not a whole-site or paid-plan allocation.
|
|
64
|
+
export const INTERNAL_LINK_SCAN_LIMITS=Object.freeze({pagesPerRun:300,workspacePagesPerDay:300,retainedRunsAndSnapshotsPerProject:20,
|
|
65
|
+
retentionDays:30,pageBytes:2*1024*1024,pageTimeoutMs:20_000,leaseMs:90_000,maxAttempts:3,edgesPerPage:1000,pageSize:100});
|
|
66
|
+
const grantTierLimit=z.union([z.literal(300),z.literal(1000),z.literal(10000),z.literal(100000)]);
|
|
67
|
+
const scanLimits=z.strictObject(Object.fromEntries(Object.entries(INTERNAL_LINK_SCAN_LIMITS).map(([k,v])=>[k,['pagesPerRun','workspacePagesPerDay'].includes(k)?grantTierLimit:z.literal(v)])));
|
|
68
|
+
const scanCoverage=z.strictObject({inventory:z.literal('partial'),completeSiteCensus:z.literal(false),representation:z.literal('raw_html'),frontierTruncated:z.boolean(),renderedVisibility:z.literal(false),canonicalAliasesMerged:z.literal(false)});
|
|
69
|
+
const inventoryCounts=z.strictObject({declared:n,excluded:n,checked:n,unknown:n,pending:n,overlapWithCrawl:n,declaredOnlyWithinObservedDiscovery:n,notAdmittedByReason:z.record(z.string(),n)});
|
|
70
|
+
const scanInventory=z.strictObject({snapshots:z.array(z.strictObject({snapshotId:id,revision:n.describe('Revision of the saved inventory represented in this scan.'),createdAt:time,expiresAt:time,declaredComplete:z.boolean(),rows:n,excluded:n,counts:inventoryCounts.optional()})).max(20),seeded:z.boolean(),crawlOnly:n.optional(),completeSiteCensus:z.literal(false)});
|
|
71
|
+
export const internalLinkScan=z.strictObject({version:z.enum(['internal-link-scan-v1','internal-link-scan-qualification-v2']),id,projectId:id,domain:z.string(),homepage:z.string(),
|
|
72
|
+
state:z.enum(['queued','running','paused','cancelled','complete','limit_reached','failed']),reason:z.string().nullable(),createdAt:time,updatedAt:time,expiresAt:time,completedAt:time.nullable(),revision:n,controlRevision:n,
|
|
73
|
+
discoveredCount:n,completeCount:n,unknownCount:n,pendingCount:n,admittedCount:n,edgeCount:n,excludedReferences:n,exclusions:z.record(z.string(),n),nextEligibleAt:time,
|
|
74
|
+
discovery:z.strictObject({state:z.enum(['pending','complete']),coverage:discoveryCoverage.optional(),documents:z.array(discoveryDocument).max(13).optional(),inventory:scanInventory.optional()}),coverage:scanCoverage,
|
|
75
|
+
usage:z.strictObject({unit:z.literal('admitted_page'),period:time,limit:grantTierLimit,used:n,resetsAt:time})});
|
|
76
|
+
export const internalLinkScanOutput=z.strictObject({scan:internalLinkScan,limits:scanLimits});
|
|
77
|
+
export const internalLinkScanSchedule=z.strictObject({cadence:z.enum(INTERNAL_LINK_SCAN_CADENCES),pageLimit:grantTierLimit.nullable(),nextRunAt:time.nullable(),updatedAt:time.nullable(),
|
|
78
|
+
lastRunId:z.string().nullable(),lastStartedAt:time.nullable(),lastError:z.string().nullable()});
|
|
79
|
+
export const internalLinkScanScheduleOutput=z.strictObject({schedule:internalLinkScanSchedule});
|
|
80
|
+
export const internalLinkScanListOutput=z.strictObject({items:z.array(internalLinkScan).max(20),limits:scanLimits,schedule:internalLinkScanSchedule.optional()});
|
|
81
|
+
const scanSource=z.enum(['homepage','sitemap','link','search_console',...INTERNAL_LINK_SOURCES]);
|
|
82
|
+
const inventoryRef=z.strictObject({snapshotId:id,source:z.enum(INTERNAL_LINK_SOURCES),important:z.boolean(),excluded:z.boolean()});
|
|
83
|
+
const inventoryRow=inventoryRef.extend({url:z.string(),state:z.enum(['pending','complete','unknown','excluded','not_admitted']),reason:z.string().nullable(),observedState:z.enum(['pending','complete','unknown']).nullable(),observedReason:z.string().nullable(),observedInLinksOrSitemaps:z.boolean()});
|
|
84
|
+
const searchMetrics=z.strictObject({clicks:n,impressions:n,position:z.number().nonnegative().nullable()});
|
|
85
|
+
// linkScore: this site's internal link share on a 0-100 log scale; not Google PageRank.
|
|
86
|
+
const scanPage=internalLinkPage.extend({renderReason:z.string().optional(),source:scanSource,inventorySources:z.array(scanSource).min(1).max(10).optional(),inventoryRefs:z.array(inventoryRef).max(20).optional(),discoveredDepth:n,linkScore:n.max(100).nullable().optional(),search:searchMetrics.nullable().optional(),pageHealth:z.enum(INTERNAL_LINK_PAGE_HEALTH_BUCKETS).optional()});
|
|
87
|
+
const searchEvidence=z.strictObject({clicks:n,impressions:n,position:z.number().nonnegative().nullable(),startDate:z.string().nullable(),endDate:z.string().nullable(),source:z.enum(['live','stored'])});
|
|
88
|
+
const scanFinding=internalLinkFinding.extend({kind:z.enum([...internalLinkFinding.shape.kind.options,'search_orphan','search_page_missing','disconnected_section']),
|
|
89
|
+
evidence:findingEvidence.extend({inventorySource:scanSource,search:searchEvidence.optional(),
|
|
90
|
+
redirectChain:z.array(z.strictObject({from:z.string(),to:z.string(),status:n})).max(20).optional(),
|
|
91
|
+
section:z.strictObject({segment:z.string(),pages:n,contextualLinksWithin:n,contextualLinksAcross:z.literal(0),classification:z.literal('url_path'),semanticGroups:z.literal('not_assessed')}).optional()})});
|
|
92
|
+
const searchOverlay=z.strictObject({state:z.enum(['live','stored','unavailable','not_connected']),reason:z.string().nullable(),property:z.string().nullable().optional(),
|
|
93
|
+
startDate:z.string().nullable().optional(),endDate:z.string().nullable().optional(),pages:n.optional(),queries:n.optional(),seeded:n.optional()});
|
|
94
|
+
export const INTERNAL_LINK_CHANGE_KINDS=Object.freeze(['page_new','page_gone','page_broken','page_fixed','page_redirected','lost_all_links_in','gained_first_link_in','links_in_changed','link_score_changed','finding_new','finding_resolved']);
|
|
95
|
+
const changesSummary=z.strictObject({state:z.enum(['ready','no_baseline','unavailable']),baselineScanId:z.string().nullable(),baselineCreatedAt:time.optional(),baselineCompletedAt:time.nullable().optional(),
|
|
96
|
+
comparableInbound:z.boolean().optional(),reason:z.string().optional(),counts:z.array(z.strictObject({kind:z.enum(INTERNAL_LINK_CHANGE_KINDS),count:n})).max(20).optional()});
|
|
97
|
+
const scanSummary=internalLinkSummary.extend({search:searchOverlay.optional(),changes:changesSummary.optional(),pageHealth:internalLinkPageHealthSummary.optional(),
|
|
98
|
+
render:z.strictObject({budget:n,used:n,failed:n,attempts:n.describe('Attempts counted against this scan\'s browser rendering budget, including failures, uncertain outcomes and conservative legacy counts.').optional(),unknown:n.optional(),legacyUncertain:z.boolean().optional()}).optional(),
|
|
99
|
+
depths:z.array(z.strictObject({depth:n.nullable(),count:n})).max(50).optional(),linkScores:z.array(z.strictObject({from:n,to:n,count:n})).max(10).optional(),opportunities:n.optional()});
|
|
100
|
+
const changeFacts=z.strictObject({state:z.string().optional(),httpStatus:n.nullable().optional(),finalUrl:z.string().optional(),incomingAll:n.optional(),incomingContextual:n.optional(),linkScore:n.nullable().optional()});
|
|
101
|
+
export const internalLinkChange=z.strictObject({id:z.string(),kind:z.enum(INTERNAL_LINK_CHANGE_KINDS),url:z.string(),before:changeFacts.optional(),after:changeFacts.optional(),delta:z.number().int().optional(),
|
|
102
|
+
finding:z.strictObject({id:z.string(),kind:z.string(),priority:z.enum(['high','medium','low']),message:z.string()}).optional()});
|
|
103
|
+
export const internalLinkSiteMap=z.strictObject({
|
|
104
|
+
sections:z.array(z.strictObject({segment:z.string(),pages:n,complete:n,topLinkScore:n.nullable()})).max(60),
|
|
105
|
+
sectionLinks:z.array(z.strictObject({source:z.string(),target:z.string(),links:n,contextualLinks:n})).max(2000),
|
|
106
|
+
pages:z.array(z.strictObject({url:z.string(),title:z.string().nullable(),segment:z.string(),linkScore:n.nullable(),incomingContextual:n,depth:n.nullable()})).max(120),
|
|
107
|
+
links:z.array(z.strictObject({sourceUrl:z.string(),targetUrl:z.string(),contextual:z.boolean()})).max(2000),linksTotal:n,pagesTotal:n});
|
|
108
|
+
export const internalLinkScanQueryOutput=z.strictObject({scan:internalLinkScan,limits:scanLimits,items:z.array(z.union([scanPage,internalLinkEdge,scanFinding,scanSummary,internalLinkCluster,internalLinkNeighborhood,internalLinkAction,internalLinkChange,internalLinkSiteMap,inventoryRow])).max(100),total:n,nextCursor:z.string().nullable(),coverage:scanCoverage});
|
|
109
|
+
// Selected changes (DP-0076-T05/T07): the customer or agent selects, the verifier confirms.
|
|
110
|
+
// The const casts keep literal types for the console, which infers its types from these schemas.
|
|
111
|
+
export const INTERNAL_LINK_CHANGE_STATES=Object.freeze(/** @type {const} */(['selected','published','verified','not_found','dismissed','resolved','stale']));
|
|
112
|
+
export const INTERNAL_LINK_DISMISS_REASONS=Object.freeze(/** @type {const} */(['intended','not_useful','wrong','other']));
|
|
113
|
+
export const INTERNAL_LINK_CHANGE_ACTIONS=Object.freeze(/** @type {const} */(['select','dismiss','published','reopen']));
|
|
114
|
+
const changeVerification=z.strictObject({checkedAt:time,method:z.enum(['page_check','scan']),scanId:z.string().optional(),requestId:z.string().optional(),state:z.enum(['present','absent','unknown']),
|
|
115
|
+
reason:z.string().optional(),anchorFound:z.string().optional(),anchorMatches:z.boolean().optional(),finalUrl:z.string().nullable().optional(),captureSha256:z.string().nullable().optional(),
|
|
116
|
+
targetHttpStatus:n.nullable().optional(),targetWorks:z.boolean().nullable().optional()});
|
|
117
|
+
const actorRef=z.strictObject({kind:z.enum(['member','agent']),id:z.string()});
|
|
118
|
+
export const internalLinkEdit=z.strictObject({id:z.string(),projectId:id,state:z.enum(INTERNAL_LINK_CHANGE_STATES),revision:z.number().int().positive().describe('Revision of this saved internal-link change.'),
|
|
119
|
+
subject:z.strictObject({kind:z.enum(['suggestion','finding','action']),id:z.string(),findingKind:z.string().nullable(),sourceUrl:z.string().nullable(),targetUrl:z.string().nullable(),pageUrl:z.string(),
|
|
120
|
+
message:z.string().max(1000).nullable().describe('The suggestion or finding message, captured when it was selected.'),goal:z.enum(['link','repair','unlink','page'])}),
|
|
121
|
+
edit:z.strictObject({anchor:z.string().nullable(),passage:z.string().nullable(),matchStart:n.nullable(),matchEnd:n.nullable(),phraseSource:z.enum(['search_query','title','address']).nullable(),
|
|
122
|
+
anchorBy:z.enum(['product','customer','agent']).nullable(),anchorRef:z.string().nullable()}),
|
|
123
|
+
origin:z.strictObject({scanId:z.string(),sourceCheckedAt:time.nullable(),sourceCaptureSha256:z.string().nullable()}),
|
|
124
|
+
dismissReason:z.enum(INTERNAL_LINK_DISMISS_REASONS).nullable(),note:z.string().nullable(),externalRef:z.string().nullable(),selectedBy:actorRef,updatedBy:actorRef,
|
|
125
|
+
verification:changeVerification.nullable(),verifiedAt:time.nullable(),lastCheckedAt:time.nullable(),nextCheckAt:time.nullable(),createdAt:time,updatedAt:time});
|
|
126
|
+
export const internalLinkEditOutput=z.strictObject({change:internalLinkEdit});
|
|
127
|
+
const changeCounts=z.strictObject(Object.fromEntries(INTERNAL_LINK_CHANGE_STATES.map(s=>[s,n])));
|
|
128
|
+
export const internalLinkEditListOutput=z.strictObject({items:z.array(internalLinkEdit).max(100),total:n,nextCursor:z.string().nullable(),counts:z.strictObject({byState:changeCounts})});
|
|
129
|
+
const openFinding=z.strictObject({id:z.string(),kind:z.string(),priority:z.enum(['high','medium','low']),pageUrl:z.string(),targetUrl:z.string().optional(),message:z.string()});
|
|
130
|
+
const scanReport=z.strictObject({scanId:z.string(),completedAt:time,layout:n,pages:z.strictObject({total:n,checked:n,unknown:n}),linksTotal:n,truncated:z.boolean(),
|
|
131
|
+
findings:z.record(z.string(),n),openFindings:z.array(openFinding).max(50),opportunitiesTotal:n,
|
|
132
|
+
changes:z.strictObject({state:z.enum(['ready','no_baseline','unavailable']),baselineScanId:z.string().nullable(),reason:z.string().optional(),counts:z.array(z.strictObject({kind:z.string(),count:n})).max(20).optional()}).nullable(),
|
|
133
|
+
search:z.strictObject({state:z.string(),reason:z.string().nullable(),pages:n.optional(),queries:n.optional(),seeded:n.optional()}).nullable()});
|
|
134
|
+
export const internalLinkReportOutput=z.strictObject({report:scanReport.nullable(),baseline:scanReport.nullable(),comparable:z.boolean(),reason:z.enum(['no_report','no_baseline','truncated']).nullable(),
|
|
135
|
+
differences:z.array(z.strictObject({kind:z.string(),before:n,after:n,delta:z.number().int()})).max(50),
|
|
136
|
+
unknown:z.strictObject({report:n.nullable(),baseline:n.nullable()}),
|
|
137
|
+
changes:z.strictObject({window:z.strictObject({from:time.nullable(),to:time}),counts:changeCounts,
|
|
138
|
+
dismissals:z.array(z.strictObject({reason:z.enum(INTERNAL_LINK_DISMISS_REASONS).nullable(),by:z.enum(['member','agent']),findingKind:z.string().nullable(),count:n})).max(100),
|
|
139
|
+
// Outcome rows only; list_internal_link_changes has each change in full.
|
|
140
|
+
items:z.array(z.strictObject({id:z.string(),state:z.enum(INTERNAL_LINK_CHANGE_STATES),goal:z.enum(['link','repair','unlink','page']),subjectKind:z.enum(['suggestion','finding','action']),findingKind:z.string().nullable(),
|
|
141
|
+
message:z.string().nullable(),pageUrl:z.string(),sourceUrl:z.string().nullable(),targetUrl:z.string().nullable(),anchor:z.string().nullable(),
|
|
142
|
+
dismissReason:z.enum(INTERNAL_LINK_DISMISS_REASONS).nullable(),note:z.string().nullable(),by:z.enum(['member','agent']),checkedBy:z.enum(['page_check','scan']).nullable(),verifiedAt:time.nullable(),updatedAt:time})).max(100),truncated:z.boolean()}),
|
|
143
|
+
trend:z.array(z.strictObject({scanId:z.string(),completedAt:time,truncated:z.boolean(),pagesChecked:n,pagesUnknown:n,findings:z.record(z.string(),n)})).max(20)});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
import {LINK_TYPES} from './link-labels.js';
|
|
3
|
+
import {LINK_CATEGORY_CONTRACT} from './link-category.js';
|
|
4
|
+
|
|
5
|
+
const text=z.string().nullable(), amount=z.number().int().nonnegative().nullable();
|
|
6
|
+
const coverage=z.looseObject({assessed:amount,classified:amount,eligible:amount,retained:amount,excluded:amount,unassessed:amount,unclassified:amount,partial:z.boolean().nullable()})
|
|
7
|
+
.describe('Occurrence counts for this watch. Null means unavailable. Assessed counts completed evaluations, including inconclusive ones. Classified counts occurrences with a determined role; partial describes type coverage. Retained includes excluded occurrences. Counts do not represent separate watched links.');
|
|
8
|
+
const role=z.looseObject({id:z.enum(LINK_TYPES),name:z.string(),standard_name:z.string(),count:z.number().int().nonnegative(),
|
|
9
|
+
states:z.looseObject({suggested:z.number().int().nonnegative(),accepted_manual:z.number().int().nonnegative(),accepted_auto:z.number().int().nonnegative()})});
|
|
10
|
+
export const linkCategoryFields={
|
|
11
|
+
category_contract:z.literal(LINK_CATEGORY_CONTRACT).optional().describe('Version of the shared table, API and export category projection.'),
|
|
12
|
+
chosen_category:z.looseObject({intent:z.enum(['chosen','cleared','automatic']),id:z.enum(LINK_TYPES).nullable(),name:text,standard_name:text,
|
|
13
|
+
actor_id:text,recorded_at:text,revision:z.number().int().nonnegative().describe('Saved label revision; pass as revision to set_link_type. Required for intent=automatic.'),deliberate_clear:z.boolean()}).optional()
|
|
14
|
+
.describe('The saved choice and recorded account attribution. Cleared keeps the type empty; automatic permits eligible existing page evidence to supply the displayed category. Neither action reviews or starts AI work.'),
|
|
15
|
+
page_assessment:z.looseObject({status:z.enum(['ready','unavailable']),reason:z.string(),read_reason:text,explanation:z.string(),applicability:z.string(),
|
|
16
|
+
binding:z.looseObject({workspaceId:z.string(),projectId:z.string(),watchId:z.string()}).nullable(),
|
|
17
|
+
capture:z.looseObject({id:z.string(),manifest_id:text,observation_id:text,observed_at:text,expires_at:text}).nullable(),
|
|
18
|
+
source:z.looseObject({state:text,observed_at:text,observation_id:text,pageUnavailable:z.boolean()}).nullable(),
|
|
19
|
+
assessed_at:text,revision:z.number().int().nonnegative().describe('Highest retained eligible occurrence decision or review revision, or null. Evidence provenance, not a write token.').nullable(),
|
|
20
|
+
model_revisions:z.array(z.looseObject({model:z.string(),revision:z.string().describe('Recorded model version or revision, or null. Evidence provenance, not a write token.').nullable()})),
|
|
21
|
+
policy_revisions:z.array(z.looseObject({taxonomy_version:text,rubric_version:text,policy_revision:z.union([z.string(),z.number()]).nullable().describe('Recorded policy revision as a string or number, or null. Evidence provenance, not a write token.')})),
|
|
22
|
+
roles:z.array(role),decision_state:z.enum(['suggested','accepted_manual','accepted_auto','mixed','none']),mixed:z.boolean(),coverage,
|
|
23
|
+
pending_count:amount,failed_count:amount,job_count:amount,review_required_count:amount}).optional()
|
|
24
|
+
.describe('What retained page evidence says about link occurrences. Applicability separates current, previous, expired and unavailable evidence. Suggested is provisional; accepted_manual requires a recorded review; accepted_auto requires a qualified saved policy.'),
|
|
25
|
+
display_category:z.looseObject({source:z.enum(['chosen','page_assessment','none']),kind:z.enum(['category','mixed','cleared','unknown']),
|
|
26
|
+
id:z.enum(LINK_TYPES).nullable(),name:z.string(),state:z.string(),reason:z.string(),applicability:z.string(),coverage}).optional()
|
|
27
|
+
.describe('The category shown in the everyday table. A deliberate saved choice takes precedence. Otherwise page evidence supplies a category or Mixed types, with its evidence state. Read the reason when no category is available.'),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// The same mutation receipts are consumed by command transports, REST clients
|
|
31
|
+
// and generated documentation. These are label changes, not occurrence reviews.
|
|
32
|
+
export const linkLabelledWatchResponse=z.looseObject({...linkCategoryFields,id:z.string(),type:z.string().nullable(),
|
|
33
|
+
type_source:z.enum(['model','person']).nullable(),tags:z.array(z.looseObject({id:z.string(),name:z.string(),color:z.string()})),
|
|
34
|
+
disavow:z.enum(['active','proposed']).nullable()});
|
|
35
|
+
export const linkLabelledWatchesResponse=z.looseObject({updated:z.number().int(),items:z.array(linkLabelledWatchResponse)});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {LINK_TYPE_NAMES, LINK_TYPES} from './link-labels.js';
|
|
2
|
+
|
|
3
|
+
export const LINK_CATEGORY_CONTRACT = 'link-category-v1';
|
|
4
|
+
const states = ['suggested', 'accepted_manual', 'accepted_auto'];
|
|
5
|
+
const count = value => Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
6
|
+
const date = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) ? value : null;
|
|
7
|
+
const name = (id, alias) => alias || LINK_TYPE_NAMES[id] || id;
|
|
8
|
+
|
|
9
|
+
/** Group roles before comparing counts. Decision states remain independent within each role. */
|
|
10
|
+
export function groupAssessmentRoles(roles = []) {
|
|
11
|
+
const groups = new Map();
|
|
12
|
+
for (const role of roles) {
|
|
13
|
+
if (!LINK_TYPES.includes(role.id) || role.id === 'guest_post' || !states.includes(role.state) || !count(role.count)) continue;
|
|
14
|
+
const group = groups.get(role.id) ?? {id: role.id, name: name(role.id, role.label), standard_name: name(role.id), count: 0,
|
|
15
|
+
states: {suggested: 0, accepted_manual: 0, accepted_auto: 0}};
|
|
16
|
+
group.count += role.count; group.states[role.state] += role.count; groups.set(role.id, group);
|
|
17
|
+
}
|
|
18
|
+
return [...groups.values()].sort((a, b) => b.count - a.count || a.id.localeCompare(b.id));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const decisionState = roles => {
|
|
22
|
+
const present = states.filter(state => roles.some(role => role.states[state] > 0));
|
|
23
|
+
return present.length === 1 ? present[0] : present.length ? 'mixed' : 'none';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const reasonNames = {
|
|
27
|
+
CHOSEN_CATEGORY: 'Saved choice', DELIBERATELY_CLEARED: 'No type', CURRENT_ASSESSMENT: 'Current page assessment',
|
|
28
|
+
PREVIOUS_ASSESSMENT: 'Previous assessment', EVIDENCE_EXPIRED: 'Evidence expired', EVIDENCE_NOT_AVAILABLE: 'Evidence not available',
|
|
29
|
+
PAGE_UNAVAILABLE: 'Page unavailable', LATEST_SOURCE_UNCONFIRMED: 'Latest page check inconclusive',
|
|
30
|
+
NOT_CLASSIFIED: 'Not started', CLASSIFICATION_PENDING: 'Assessment pending', CLASSIFICATION_FAILED: 'Classification failed',
|
|
31
|
+
CLASSIFICATION_UNAVAILABLE: 'Could not load assessment', NO_ELIGIBLE_OCCURRENCES: 'No eligible occurrences',
|
|
32
|
+
NO_ROLE_DETERMINED: 'No page role determined', LINK_ABSENT: 'Link missing in latest check',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Effect-free watch-level category projection. Persisted model labels are compatibility caches only. */
|
|
36
|
+
export function resolveLinkCategory(row = {}) {
|
|
37
|
+
const summary = row.classification, available = summary?.status === 'ready';
|
|
38
|
+
const applicability = summary?.applicability ?? 'unavailable';
|
|
39
|
+
const roles = available ? groupAssessmentRoles(summary.roles) : [];
|
|
40
|
+
const classified = available ? roles.reduce((sum, role) => sum + role.count, 0) : null;
|
|
41
|
+
const assessed = available ? count(summary.assessmentCount) ?? classified : null;
|
|
42
|
+
const eligible = available ? count(summary.eligibleCount ?? summary.count) : null;
|
|
43
|
+
const retained = available ? count(summary.retainedCount) : null;
|
|
44
|
+
const coverage = {assessed, classified, eligible, retained, excluded: available ? count(summary.excludedCount) : null,
|
|
45
|
+
unassessed: eligible === null ? null : Math.max(0, eligible - assessed),
|
|
46
|
+
unclassified: eligible === null ? null : Math.max(0, eligible - classified),
|
|
47
|
+
partial: eligible === null ? null : classified > 0 && classified < eligible};
|
|
48
|
+
let reason = !available ? 'CLASSIFICATION_UNAVAILABLE'
|
|
49
|
+
: applicability === 'expired_source_evidence' ? 'EVIDENCE_EXPIRED'
|
|
50
|
+
: applicability === 'unavailable_source_evidence' ? 'EVIDENCE_NOT_AVAILABLE'
|
|
51
|
+
: roles.length && applicability !== 'current' ? 'PREVIOUS_ASSESSMENT'
|
|
52
|
+
: roles.length ? 'CURRENT_ASSESSMENT'
|
|
53
|
+
: applicability === 'historical_absent' ? 'LINK_ABSENT'
|
|
54
|
+
: applicability === 'unconfirmed_latest_source' ? (summary.source?.pageUnavailable ? 'PAGE_UNAVAILABLE' : 'LATEST_SOURCE_UNCONFIRMED')
|
|
55
|
+
: summary.pendingCount > 0 ? 'CLASSIFICATION_PENDING'
|
|
56
|
+
: summary.failedCount > 0 ? 'CLASSIFICATION_FAILED'
|
|
57
|
+
: summary.capture && eligible === 0 ? 'NO_ELIGIBLE_OCCURRENCES'
|
|
58
|
+
: summary.assessmentCount > 0 || summary.jobCount > 0 ? 'NO_ROLE_DETERMINED' : 'NOT_CLASSIFIED';
|
|
59
|
+
const page = {status: available ? 'ready' : 'unavailable', reason, read_reason: summary?.reason ?? null,
|
|
60
|
+
explanation: reasonNames[reason], applicability, binding: summary?.binding ?? null, capture: summary?.capture ?? null,
|
|
61
|
+
source: summary?.source ?? null, assessed_at: date(summary?.assessedAt), revision: summary?.revision ?? null,
|
|
62
|
+
model_revisions: summary?.modelRevisions ?? [], policy_revisions: summary?.policyRevisions ?? [],
|
|
63
|
+
roles, decision_state: decisionState(roles), mixed: roles.length > 1, coverage,
|
|
64
|
+
pending_count: available ? count(summary.pendingCount) : null, failed_count: available ? count(summary.failedCount) : null,
|
|
65
|
+
job_count: available ? count(summary.jobCount) : null,
|
|
66
|
+
review_required_count: available ? count(summary.reviewRequiredCount) : null};
|
|
67
|
+
const deliberate = row.type_source === 'person';
|
|
68
|
+
const chosenId = deliberate && LINK_TYPES.includes(row.type) ? row.type : null;
|
|
69
|
+
const alias = summary?.roleAliases?.[chosenId];
|
|
70
|
+
const chosen = {intent: deliberate ? chosenId === null ? 'cleared' : 'chosen' : 'automatic',
|
|
71
|
+
id: chosenId, name: chosenId === null ? null : name(chosenId, alias), standard_name: chosenId === null ? null : name(chosenId),
|
|
72
|
+
actor_id: deliberate ? row.type_set_by ?? null : null, recorded_at: deliberate ? date(row.type_set_at) : null,
|
|
73
|
+
revision: row.type_revision ?? 0, deliberate_clear: deliberate && chosenId === null};
|
|
74
|
+
const display = {source: deliberate ? 'chosen' : roles.length ? 'page_assessment' : 'none',
|
|
75
|
+
kind: deliberate ? chosenId === null ? 'cleared' : 'category' : roles.length > 1 ? 'mixed' : roles.length ? 'category' : 'unknown',
|
|
76
|
+
id: deliberate ? chosenId : roles.length === 1 ? roles[0].id : null,
|
|
77
|
+
name: deliberate ? chosen.name ?? 'No type' : roles.length > 1 ? 'Mixed types' : roles.length ? roles[0].name : reasonNames[reason],
|
|
78
|
+
state: deliberate ? chosen.intent : page.decision_state,
|
|
79
|
+
reason: deliberate ? chosenId === null ? 'DELIBERATELY_CLEARED' : 'CHOSEN_CATEGORY' : reason,
|
|
80
|
+
applicability: deliberate ? 'chosen' : applicability, coverage};
|
|
81
|
+
return {category_contract: LINK_CATEGORY_CONTRACT, chosen_category: chosen, page_assessment: page, display_category: display};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Validate the server projection without recomputing its semantics in a client. */
|
|
85
|
+
export function readLinkCategory(row, workspaceId = row?.workspace_id) {
|
|
86
|
+
if (row?.category_contract !== LINK_CATEGORY_CONTRACT) return null;
|
|
87
|
+
const {chosen_category: chosen, page_assessment: page, display_category: display} = row;
|
|
88
|
+
const identity = value => typeof value === 'string' && value.trim().length > 0;
|
|
89
|
+
if (![workspaceId, row.workspace_id, row.project_id, row.id].every(identity) || workspaceId !== row.workspace_id) return null;
|
|
90
|
+
if (!chosen || !page || !display || !page.binding || page.binding.workspaceId !== workspaceId
|
|
91
|
+
|| page.binding.watchId !== row.id || page.binding.projectId !== row.project_id) return null;
|
|
92
|
+
if (!['chosen', 'cleared', 'automatic'].includes(chosen.intent) || !['ready', 'unavailable'].includes(page.status)
|
|
93
|
+
|| !['chosen', 'page_assessment', 'none'].includes(display.source) || !['category', 'mixed', 'cleared', 'unknown'].includes(display.kind)
|
|
94
|
+
|| !identity(display.name) || (display.id !== null && !LINK_TYPES.includes(display.id)) || !Array.isArray(page.roles)
|
|
95
|
+
|| (chosen.id !== null && (!LINK_TYPES.includes(chosen.id) || !identity(chosen.name) || !identity(chosen.standard_name)))
|
|
96
|
+
|| !['suggested', 'accepted_manual', 'accepted_auto', 'mixed', 'none'].includes(page.decision_state)
|
|
97
|
+
|| count(chosen.revision) === null || typeof chosen.deliberate_clear !== 'boolean') return null;
|
|
98
|
+
if (page.roles.some(role => !LINK_TYPES.includes(role.id) || role.id === 'guest_post' || typeof role.name !== 'string'
|
|
99
|
+
|| !identity(role.name) || !identity(role.standard_name) || !count(role.count) || !role.states || states.some(state => count(role.states[state]) === null)
|
|
100
|
+
|| states.reduce((sum, state) => sum + role.states[state], 0) !== role.count)) return null;
|
|
101
|
+
if (new Set(page.roles.map(role => role.id)).size !== page.roles.length) return null;
|
|
102
|
+
const coverage = page.coverage;
|
|
103
|
+
if (!coverage || ['assessed', 'classified', 'eligible', 'retained', 'excluded', 'unassessed', 'unclassified'].some(key => coverage[key] !== null && count(coverage[key]) === null)
|
|
104
|
+
|| coverage.partial !== null && typeof coverage.partial !== 'boolean') return null;
|
|
105
|
+
if (page.status === 'ready' && (coverage.classified !== page.roles.reduce((sum, role) => sum + role.count, 0)
|
|
106
|
+
|| coverage.assessed < coverage.classified || coverage.eligible !== null && (coverage.eligible < coverage.assessed
|
|
107
|
+
|| coverage.unassessed !== coverage.eligible - coverage.assessed || coverage.unclassified !== coverage.eligible - coverage.classified)
|
|
108
|
+
|| coverage.retained !== null && coverage.eligible !== null && coverage.retained !== coverage.eligible + coverage.excluded)) return null;
|
|
109
|
+
if (chosen.intent === 'chosen' && (chosen.id === null || chosen.deliberate_clear)
|
|
110
|
+
|| chosen.intent === 'cleared' && (chosen.id !== null || !chosen.deliberate_clear)
|
|
111
|
+
|| chosen.intent === 'automatic' && (chosen.id !== null || chosen.deliberate_clear)) return null;
|
|
112
|
+
return {category_contract: row.category_contract, chosen_category: chosen, page_assessment: page, display_category: display};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** New category filters have explicit watch/occurrence scope; the old `type` filter is unchanged. */
|
|
116
|
+
export function matchesLinkCategory(row, {category, categoryState, categoryScope = 'shown', assessmentScope = 'current'} = {}) {
|
|
117
|
+
const projection = row.category_contract === LINK_CATEGORY_CONTRACT ? row : {...row, ...resolveLinkCategory(row)};
|
|
118
|
+
const page = projection.page_assessment, display = projection.display_category;
|
|
119
|
+
const previous = page.status === 'ready' && page.applicability !== 'current' && (page.capture !== null || page.roles.length > 0);
|
|
120
|
+
const inScope = page.status === 'ready' && (assessmentScope === 'current' ? page.applicability === 'current' : previous);
|
|
121
|
+
const stateMatches = !categoryState || ({cleared: projection.chosen_category.deliberate_clear,
|
|
122
|
+
not_started: page.reason === 'NOT_CLASSIFIED' && ['current', 'not_prepared'].includes(page.applicability),
|
|
123
|
+
pending: inScope && page.pending_count > 0, classifier_failed: inScope && page.failed_count > 0,
|
|
124
|
+
classifier_unavailable: page.status === 'unavailable', page_unavailable: page.status === 'ready' && page.source?.pageUnavailable === true,
|
|
125
|
+
partial: inScope && page.coverage.partial === true, previous: previous && page.roles.length > 0})[categoryState] === true;
|
|
126
|
+
if (!stateMatches) return false;
|
|
127
|
+
if (category === undefined || category === null) return true;
|
|
128
|
+
if (categoryScope === 'page_role') {
|
|
129
|
+
if (!inScope) return false;
|
|
130
|
+
if (category === 'none') return page.roles.length === 0;
|
|
131
|
+
if (category === 'mixed') return page.mixed;
|
|
132
|
+
return page.roles.some(role => role.id === category);
|
|
133
|
+
}
|
|
134
|
+
if (display.source === 'page_assessment' && (assessmentScope === 'current' ? display.applicability !== 'current' : display.applicability === 'current')) return false;
|
|
135
|
+
if (assessmentScope === 'previous' && display.source !== 'page_assessment') return false;
|
|
136
|
+
return category === 'none' ? display.kind === 'unknown' || display.kind === 'cleared'
|
|
137
|
+
: category === 'mixed' || category === 'cleared' ? display.kind === category : display.id === category;
|
|
138
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// One person-facing link type per monitored link, grouped into four families for the badge
|
|
2
|
+
// dot. The ids are the canonical classification roles plus `guest_post`, which only a
|
|
3
|
+
// person (or an agent acting for one) sets; the model's taxonomy is unchanged.
|
|
4
|
+
export const LINK_TYPE_FAMILIES = Object.freeze([
|
|
5
|
+
{id: 'editorial', label: 'In content', types: ['contextual_citation', 'guest_post', 'recommendation', 'resource_entry', 'technical_reference']},
|
|
6
|
+
{id: 'listing', label: 'Listings', types: ['directory_entry']},
|
|
7
|
+
{id: 'community', label: 'Profiles and community', types: ['profile_website', 'author_attribution', 'discussion_contribution']},
|
|
8
|
+
{id: 'sitewide', label: 'Site-wide', types: ['navigation_template', 'badge_embed_attribution', 'partner_sponsor']},
|
|
9
|
+
{id: 'other', label: '', types: ['other']},
|
|
10
|
+
]);
|
|
11
|
+
export const LINK_TYPES = Object.freeze(LINK_TYPE_FAMILIES.flatMap(family => family.types));
|
|
12
|
+
export const LINK_TYPE_NAMES = Object.freeze({
|
|
13
|
+
contextual_citation: 'Reference in content', guest_post: 'Guest post', recommendation: 'Recommendation',
|
|
14
|
+
resource_entry: 'Resource list entry', technical_reference: 'Technical reference', directory_entry: 'Directory listing',
|
|
15
|
+
profile_website: 'Profile link', author_attribution: 'Author credit or bio', discussion_contribution: 'Comment or forum post',
|
|
16
|
+
navigation_template: 'Navigation or footer', badge_embed_attribution: 'Embedded credit', partner_sponsor: 'Partner or sponsor',
|
|
17
|
+
other: 'Other',
|
|
18
|
+
});
|
|
19
|
+
export const linkTypeFamily = type => LINK_TYPE_FAMILIES.find(family => family.types.includes(type))?.id ?? 'other';
|
|
20
|
+
/**
|
|
21
|
+
* Whole site vs one page by default, from the link type: disavowing all of medium.com or
|
|
22
|
+
* patreon.com for one profile or comment is almost always a mistake.
|
|
23
|
+
*/
|
|
24
|
+
export const PAGE_ONLY_TYPES = Object.freeze(['profile_website', 'discussion_contribution', 'author_attribution']);
|
|
25
|
+
/** The six tag colours (the light-theme text colour of each pair), in the order new tags get them. */
|
|
26
|
+
export const TAG_COLORS = Object.freeze(['#075985', '#155e75', '#6b21a8', '#3730a3', '#9d174d', '#334155']);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
// The per-website summary every console count reads (GET /v1/reports/summary, get_link_summary).
|
|
3
|
+
const count=z.number().int().nonnegative();
|
|
4
|
+
const at=z.string().nullable();
|
|
5
|
+
// last_checked_at is the oldest latest check among active links (every one was checked since);
|
|
6
|
+
// latest_check_at is the newest. Missing changes require historical presence evidence.
|
|
7
|
+
const statusCounts={prospect:count.optional(),by_intent:z.record(z.string(),count).optional(),by_outreach_status:z.record(z.string(),count).optional(),links:count,live:count,missing:count,couldnt_check:count,paused:count,not_checked_yet:count,last_checked_at:at,latest_check_at:at.optional()};
|
|
8
|
+
export const linkSummaryWebsite=z.looseObject({project_id:z.string(),domain:z.string().nullable(),name:z.string().nullable(),...statusCounts,found:count.nullable(),back:count.nullable().optional(),lost:count.nullable(),went_missing:count.nullable().optional()});
|
|
9
|
+
export const linkSummaryResponse=z.looseObject({
|
|
10
|
+
schema_version:z.literal(1),workspace_id:z.string(),project_id:z.string().nullable(),as_of:z.string(),
|
|
11
|
+
period:z.looseObject({days:z.number().int().min(1).max(90),from:z.string(),to:z.string()}),
|
|
12
|
+
totals:z.looseObject({...statusCounts,found:count.nullable(),back:count.nullable().optional(),lost:count.nullable(),went_missing:count.nullable().optional()}),
|
|
13
|
+
websites:z.array(linkSummaryWebsite),websites_truncated:z.boolean(),
|
|
14
|
+
daily:z.array(z.looseObject({day:z.string(),found:count,back:count.optional(),lost:count,went_missing:count.optional()})).nullable(),
|
|
15
|
+
changes_complete:z.boolean().nullable(),
|
|
16
|
+
statuses:z.array(z.string()),
|
|
17
|
+
});
|