agentlinkops 0.6.10 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +4 -4
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/admission.js +0 -0
- package/cli/agent-setup.js +0 -0
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +4435 -175
- package/cli/check.js +0 -0
- package/cli/chronology.js +39 -0
- package/cli/citation.js +0 -0
- package/cli/client.js +1 -1
- package/cli/cloud-error.js +0 -0
- package/cli/commands.js +0 -0
- package/cli/config.js +0 -0
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/disavow.js +0 -0
- package/cli/doctor.js +0 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +30 -19
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +4 -1
- package/cli/index-observations.js +0 -0
- package/cli/index-submissions.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/lifecycle.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +42 -15
- package/cli/mcp-client.js +2 -2
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/native-lock.js +0 -0
- package/cli/oauth-credentials.js +0 -0
- package/cli/oauth-flow.js +0 -0
- package/cli/placement-projection.js +106 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +51 -19
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +62 -29
- package/cli/status.js +14 -18
- package/cli/sync-lock.js +0 -0
- package/cli/sync.js +22 -3
- package/package.json +1 -1
- package/references/citation-panels.md +0 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +0 -0
- package/references/local-crm-draft-handoff.md +0 -0
- package/references/portable-ledger-handoff.md +0 -0
- package/references/recipes/catalog.json +0 -0
- package/references/recipes/command-notes.md +0 -0
- package/references/recipes/placement-reconciliation.md +0 -0
- package/references/recipes/qualified-campaign-handoff.md +0 -0
- package/references/recipes/site-context-brief.md +0 -0
- package/references/recipes/sourced-linkable-asset.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/outreach-crm-draft.py +0 -0
- package/scripts/portable-handoff.py +0 -0
- package/scripts/report.py +0 -0
- package/shared/admission-contract.js +0 -0
- package/shared/admission-report-contract.js +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +0 -0
- package/shared/disavow-contract.js +0 -0
- package/shared/exec-grammar.js +0 -0
- package/shared/index-observation-contract.js +2 -2
- package/shared/lifecycle-contract.js +0 -0
- package/shared/lifecycle-money.js +0 -0
- package/shared/monitoring-chronology-export.js +57 -0
- package/shared/monitoring-read-contract.js +41 -0
- package/shared/observation-event.js +0 -0
- package/shared/opportunity-record.js +0 -0
- package/shared/response-contract.js +3 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +0 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +0 -0
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +0 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +4 -3
- package/skills/agentlinkops-crm/SKILL.md +1 -1
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +0 -0
- package/src/citations/alert.js +0 -0
- package/src/citations/browser/chatgpt-citations.js +0 -0
- package/src/citations/browser/engine.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +0 -0
- package/src/citations/browser/proxy-admission.js +0 -0
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +0 -0
- package/src/citations/contract.js +0 -0
- package/src/citations/egress-receipts.js +0 -0
- package/src/citations/evidence-integrity.js +0 -0
- package/src/citations/inventory.js +0 -0
- package/src/citations/login.js +0 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +0 -0
- package/src/citations/report.js +0 -0
- package/src/citations/runner.js +0 -0
- package/src/citations/stats.js +0 -0
- package/src/citations/sync.js +0 -0
- package/src/citations/watch.js +0 -0
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +0 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/evidence-reference.js +0 -0
- package/src/index-observation.js +0 -0
- package/src/locator.js +0 -0
- package/src/response-shaping.js +0 -0
- package/src/tool-views.js +5 -4
- package/src/verifier/fetch.js +0 -0
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
package/cli/check.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {readFile,writeFile,mkdir,copyFile,unlink} from 'node:fs/promises';
|
|
2
|
+
import {constants} from 'node:fs';
|
|
3
|
+
import {dirname,resolve} from 'node:path';
|
|
4
|
+
import {loadConfig,ConfigError} from './config.js';
|
|
5
|
+
import {cloudConnection} from './connection.js';
|
|
6
|
+
import {createClient} from './client.js';
|
|
7
|
+
import {freezeChronology,normalizeLocalSubmissions} from '../shared/monitoring-chronology-export.js';
|
|
8
|
+
const help='agentlinkops chronology pull --watch-id ID [--limit 50] [--cursor TOKEN] [--all] [--out NEW_FILE] | export --pages FILE [--submissions FILE] [--out NEW_FILE]';
|
|
9
|
+
async function output(value,args,{cwd,out}){
|
|
10
|
+
const text=JSON.stringify(value,null,2)+'\n';
|
|
11
|
+
if(!args.out){out(text.trimEnd());return;}
|
|
12
|
+
if(typeof args.out!=='string'||!args.out.trim())throw new ConfigError('--out requires a file');
|
|
13
|
+
const path=resolve(cwd,args.out),temp=`${path}.${crypto.randomUUID()}.tmp`;
|
|
14
|
+
await mkdir(dirname(path),{recursive:true});
|
|
15
|
+
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;});}
|
|
16
|
+
out(`wrote ${path}`);
|
|
17
|
+
}
|
|
18
|
+
export async function chronologyMain(args,{cwd=process.cwd(),env=process.env,fetchImpl=globalThis.fetch,out=console.log}={}){
|
|
19
|
+
const command=args._[1],allowed=new Set(['_','tag','watch-id','limit','cursor','all','out','pages','submissions']);
|
|
20
|
+
if(args._.length!==2||args.tag?.length||Object.keys(args).some(key=>!allowed.has(key)))throw new ConfigError(help);
|
|
21
|
+
if(command==='export'){
|
|
22
|
+
if(typeof args.pages!=='string'||args['watch-id']!==undefined||args.limit!==undefined||args.cursor!==undefined||args.all!==undefined)throw new ConfigError(help);
|
|
23
|
+
const input=JSON.parse(await readFile(resolve(cwd,args.pages),'utf8')),pages=Array.isArray(input)?input:[input];
|
|
24
|
+
const localItems=args.submissions?await normalizeLocalSubmissions(JSON.parse(await readFile(resolve(cwd,args.submissions),'utf8')),pages[0]?.identity):[];
|
|
25
|
+
await output(await freezeChronology(pages,{localItems}),args,{cwd,out});return 0;
|
|
26
|
+
}
|
|
27
|
+
if(command!=='pull'||typeof args['watch-id']!=='string'||args.pages!==undefined||args.submissions!==undefined||args.all!==undefined&&args.all!==true)throw new ConfigError(help);
|
|
28
|
+
const limit=Number(args.limit??50);if(!Number.isInteger(limit)||limit<1||limit>100||args.cursor===true)throw new ConfigError(help);
|
|
29
|
+
const config=await loadConfig({cwd}),connection=cloudConnection(config,env);
|
|
30
|
+
if(!connection.projectId)throw new ConfigError('Connect this project before reading hosted chronology.');
|
|
31
|
+
const client=createClient({...connection,fetchImpl}),pages=[];let cursor=args.cursor;
|
|
32
|
+
do{
|
|
33
|
+
const page=await client.callCommand('get_placement_chronology',{watchId:args['watch-id'],limit,...(cursor?{cursor}: {})});
|
|
34
|
+
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.');
|
|
35
|
+
pages.push(page);cursor=page.nextCursor;
|
|
36
|
+
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.');
|
|
37
|
+
}while(args.all&&cursor);
|
|
38
|
+
await output(args.all?await freezeChronology(pages):pages[0],args,{cwd,out});return 0;
|
|
39
|
+
}
|
package/cli/citation.js
CHANGED
|
File without changes
|
package/cli/client.js
CHANGED
|
@@ -5,7 +5,7 @@ export {CloudError} from './cloud-error.js';
|
|
|
5
5
|
// A small HTTP client for the cloud. Scoped keys only; no human session ever reaches here.
|
|
6
6
|
// DP-0036-T13: the CLI names itself so the usage counters can tell it from other REST callers.
|
|
7
7
|
// The version follows the plugin manifests; bump it with them.
|
|
8
|
-
export const CLI_VERSION = '0.6.
|
|
8
|
+
export const CLI_VERSION = '0.6.13';
|
|
9
9
|
export const CLI_USER_AGENT = `agentlinkops-cli/${CLI_VERSION}`;
|
|
10
10
|
const MAX_ATTEMPTS = 3;
|
|
11
11
|
const MAX_RETRY_WAIT_MS = 5_000;
|
package/cli/cloud-error.js
CHANGED
|
File without changes
|
package/cli/commands.js
CHANGED
|
File without changes
|
package/cli/config.js
CHANGED
|
File without changes
|
package/cli/connection.js
CHANGED
|
File without changes
|
package/cli/context-mcp.js
CHANGED
|
File without changes
|
package/cli/context.js
CHANGED
|
File without changes
|
package/cli/delivery.js
CHANGED
|
File without changes
|
package/cli/disavow.js
CHANGED
|
File without changes
|
package/cli/doctor.js
CHANGED
|
File without changes
|
package/cli/env.js
CHANGED
|
File without changes
|
package/cli/fleet.js
CHANGED
|
@@ -6,54 +6,62 @@
|
|
|
6
6
|
// and a summary that deduplicated them would silently pick one. So every number below is
|
|
7
7
|
// per project, the only cross-project arithmetic is a sum of counts, and the output says so.
|
|
8
8
|
//
|
|
9
|
-
// "Earned" follows
|
|
10
|
-
//
|
|
11
|
-
// is reported as never checked rather than as zero links.
|
|
9
|
+
// "Earned" follows known conclusive presence; a later inconclusive attempt is disclosed
|
|
10
|
+
// separately. Activity is supplied explicitly per project, never discovered in another repo.
|
|
12
11
|
import { readLedger } from './ledger.js';
|
|
13
|
-
import { readObservations
|
|
12
|
+
import { readObservations } from './mirror.js';
|
|
13
|
+
import { readState } from './state.js';
|
|
14
|
+
import { projectPlacements } from './placement-projection.js';
|
|
14
15
|
|
|
15
16
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
16
17
|
|
|
17
18
|
/** One project's row. Everything is computed inside the project; nothing crosses the boundary. */
|
|
18
|
-
export async function fleetProject(name, ledgerPath, observationsPath) {
|
|
19
|
+
export async function fleetProject(name, ledgerPath, observationsPath, statePath = null) {
|
|
19
20
|
const ledger = await readLedger(ledgerPath);
|
|
20
21
|
if (ledger.missing) { const error = new Error(`no ledger at ${ledgerPath}`); error.code = 'NO_LEDGER'; throw error; }
|
|
21
22
|
const byIntent = { wanted: 0, expected: 0, retired: 0 };
|
|
22
23
|
for (const entry of ledger.entries) byIntent[entry.intent] = (byIntent[entry.intent] ?? 0) + 1;
|
|
23
24
|
|
|
24
25
|
const { rows } = observationsPath ? await readObservations(observationsPath) : { rows: [] };
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
const lastCheck = rows.length ? rows.map(row => row.checked_at).sort().at(-1) : null;
|
|
26
|
+
const state = statePath ? await readState(statePath) : null;
|
|
27
|
+
const eligible = ledger.entries.filter(entry => entry.intent !== 'retired');
|
|
28
|
+
const projected = projectPlacements(eligible, rows, { state });
|
|
29
|
+
const observed = projected.filter(item => item.latest_attempt).length;
|
|
30
|
+
const earned = projected.filter(item => item.current_state === 'present').length;
|
|
31
|
+
const newest = values => values.filter(Boolean).sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null;
|
|
33
32
|
return {
|
|
34
33
|
project: name, ledger: ledgerPath,
|
|
35
34
|
entries: ledger.entries.length, ...byIntent,
|
|
36
35
|
problems: ledger.problems.length,
|
|
37
36
|
observed_entries: observed, earned,
|
|
38
|
-
never_checked:
|
|
39
|
-
last_observation:
|
|
37
|
+
never_checked: eligible.length - observed,
|
|
38
|
+
last_observation: newest(projected.map(item => item.evidence_observed_at)),
|
|
39
|
+
last_attempt_at: newest(projected.map(item => item.latest_attempt?.checked_at)),
|
|
40
|
+
last_success_at: newest(projected.map(item => item.last_successful_observation?.checked_at)),
|
|
41
|
+
last_link_verification_at: newest(projected.map(item => item.last_link_verification?.checked_at)),
|
|
42
|
+
uncertain_entries: projected.filter(item => item.uncertain).length,
|
|
43
|
+
activity_available: projected.some(item => item.activity_available),
|
|
44
|
+
activity_entries: projected.filter(item => item.activity_available).length,
|
|
45
|
+
coverage: { denominator: 'non_retired_ledger_entries', eligible_entries: eligible.length,
|
|
46
|
+
identity_unmatched_entries: projected.filter(item => item.identity_status === 'not_matched').length,
|
|
47
|
+
activity_input: statePath ? 'explicit_state_file' : 'not_supplied' },
|
|
40
48
|
// Due volume, the same arithmetic the dogfood records use to price a fleet.
|
|
41
49
|
implied_weekly_checks: byIntent.wanted + byIntent.expected * 7,
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
/** Summons the fleet. Same numbers per project, one sum of counts, no merged intent. */
|
|
46
|
-
export async function fleetSummary(projects, { observationsOf = {} } = {}) {
|
|
54
|
+
export async function fleetSummary(projects, { observationsOf = {}, statesOf = {} } = {}) {
|
|
47
55
|
const rows = [];
|
|
48
|
-
for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null));
|
|
56
|
+
for (const { name, ledger } of projects) rows.push(await fleetProject(name, ledger, observationsOf[name] ?? null, statesOf[name] ?? null));
|
|
49
57
|
const totals = rows.reduce((acc, row) => {
|
|
50
|
-
for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'implied_weekly_checks']) {
|
|
58
|
+
for (const key of ['entries', 'wanted', 'expected', 'retired', 'earned', 'observed_entries', 'never_checked', 'uncertain_entries', 'activity_entries', 'implied_weekly_checks']) {
|
|
51
59
|
acc[key] = (acc[key] ?? 0) + row[key];
|
|
52
60
|
}
|
|
53
61
|
return acc;
|
|
54
62
|
}, {});
|
|
55
63
|
return {
|
|
56
|
-
v:
|
|
64
|
+
v: 2, projects: rows, totals,
|
|
57
65
|
// The sum answers "how big is the operation". It is not a merged view: the same platform
|
|
58
66
|
// counted in two projects is two intents, and only a human can say which one they meant.
|
|
59
67
|
intent_merged_across_repos: false,
|
|
@@ -75,6 +83,9 @@ export function renderFleet(summary, out = console.log) {
|
|
|
75
83
|
+ `${String(t.never_checked).padStart(13)} (sum of counts)`);
|
|
76
84
|
out('');
|
|
77
85
|
out(`implied weekly check volume across the fleet: ${t.implied_weekly_checks}`);
|
|
86
|
+
for (const row of summary.projects) {
|
|
87
|
+
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}`);
|
|
88
|
+
}
|
|
78
89
|
if (summary.projects.some(row => row.problems)) {
|
|
79
90
|
out(`ledger problems: ${summary.projects.map(row => `${row.project}=${row.problems}`).filter(s => !s.endsWith('=0')).join(', ')}`);
|
|
80
91
|
}
|
package/cli/gsc-browser.js
CHANGED
|
File without changes
|
package/cli/import.js
CHANGED
|
@@ -74,11 +74,14 @@ export async function readImport(path, {
|
|
|
74
74
|
// chunks, so a placement repeated in two different parts of one file is still one candidate.
|
|
75
75
|
const prepared = { accepted: [], rejected: [], duplicates: [] };
|
|
76
76
|
const seen = new Map();
|
|
77
|
+
// Candidate identity includes the run. These bounded slices describe one preview, so keep
|
|
78
|
+
// its first-slice identity throughout the file or a repeated placement gets a new ID.
|
|
79
|
+
const previewRunId = `${runId}_0`;
|
|
77
80
|
const chunks = [];
|
|
78
81
|
for (let index = 0; index < rows.length; index += DISCOVERY_LIMITS.rows) chunks.push(index);
|
|
79
82
|
for (const offset of chunks) {
|
|
80
83
|
const part = await prepareImport({
|
|
81
|
-
runId:
|
|
84
|
+
runId: previewRunId, workspaceId, projectId, supplier,
|
|
82
85
|
rows: rows.slice(offset, offset + DISCOVERY_LIMITS.rows), target: targetUrl, scope,
|
|
83
86
|
taskId: 'import_preview', retrievedAt: now, generatedAt: supplierGeneratedAt,
|
|
84
87
|
seen, rowOffset: offset,
|
|
File without changes
|
package/cli/index-submissions.js
CHANGED
|
File without changes
|
package/cli/ledger.js
CHANGED
|
File without changes
|
package/cli/lifecycle.js
CHANGED
|
File without changes
|
package/cli/linktrail.mjs
CHANGED
|
File without changes
|
package/cli/local-result.js
CHANGED
|
File without changes
|
package/cli/main.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {projectPlacements} from './placement-projection.js';
|
|
2
|
+
import {chronologyMain} from './chronology.js';
|
|
1
3
|
import { acquireSyncLock } from './sync-lock.js';
|
|
2
4
|
import {lifecycleMain} from './lifecycle.js';
|
|
3
5
|
import {admissionMain} from './admission.js';
|
|
@@ -52,6 +54,8 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
52
54
|
[--mode local|hosted|external] read-only setup plan; no account required
|
|
53
55
|
agentlinkops add --source URL --target URL [--intent wanted|expected] [--scope …]
|
|
54
56
|
[--anchor TEXT] [--rel a,b] [--ref TEXT] [--tag t --tag t] [--note TEXT]
|
|
57
|
+
agentlinkops chronology pull --watch-id ID [--limit N] [--cursor TOKEN] [--all] [--out FILE]
|
|
58
|
+
agentlinkops chronology export --pages FILE [--submissions FILE] [--out FILE]
|
|
55
59
|
agentlinkops index pull --watch-id ID [--limit N] [--before ID]
|
|
56
60
|
agentlinkops lifecycle get|update|clear|renew|report
|
|
57
61
|
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
|
|
|
63
67
|
agentlinkops import FILE --target DOMAIN [--from SUPPLIER] [--map source=COL,target=COL]
|
|
64
68
|
[--exact-url] [--no-subdomains] [--generated-at ISO] [--json]
|
|
65
69
|
agentlinkops adopt CRM.sqlite [--scope exact|domain] [--write]
|
|
66
|
-
agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--json]
|
|
70
|
+
agentlinkops fleet --project NAME=LEDGER [--project …] [--observations-of NAME=FILE] [--state-of NAME=FILE] [--json]
|
|
67
71
|
agentlinkops platforms [HOST] [--observations FILE]… [--benchmark FILE]… [--seed FILE]…
|
|
68
72
|
[--for-candidates FILE] [--json]
|
|
69
73
|
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
|
|
|
100
104
|
agentlinkops adopt-result FILE [--intent wanted|expected] save result to local ledger
|
|
101
105
|
agentlinkops receipt add --file CLAIM.json project.site declares public site hosts
|
|
102
106
|
agentlinkops receipt history [--performance FILE.json] claims, history and optional dated context
|
|
103
|
-
agentlinkops status [--json]
|
|
107
|
+
agentlinkops status [--all] [--json]
|
|
104
108
|
agentlinkops diff [--json]
|
|
105
109
|
agentlinkops report [--out FILE] [--title T] [--brand B] [--as-of ISO]
|
|
106
110
|
[--include-notes] [--include-retired] [--digest-only] [--json]
|
|
@@ -115,13 +119,14 @@ const USAGE = `agentlinkops — a backlink ledger that lives in your repository
|
|
|
115
119
|
Suppliers: ${SUPPLIER_NAMES.join(', ')}
|
|
116
120
|
A preset is a convenience over --map, never a requirement.
|
|
117
121
|
|
|
118
|
-
Exit codes: 0
|
|
119
|
-
evidence · 2 usage, configuration or ledger error.
|
|
122
|
+
Exit codes: 0 no expected link conclusively absent · 1 an expected link observed absent with complete
|
|
123
|
+
evidence · 2 usage, configuration or ledger error. Unknown is nonfailing by default;
|
|
124
|
+
--fail-on-unknown returns 1 when any observation is unknown.
|
|
120
125
|
|
|
121
126
|
Names: \`linktrail\` still runs this CLI, LINKTRAIL_* variables are still read, and an existing
|
|
122
127
|
.linktrail/ directory is still used, each with a one-line notice, until the DP-0029 cutover.`;
|
|
123
128
|
|
|
124
|
-
const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "observations", "benchmark", "seed"],{tag:[]});
|
|
129
|
+
const parseArgs = argv => parseArguments(argv,["tag", "project", "observations-of", "state-of", "observations", "benchmark", "seed"],{tag:[]});
|
|
125
130
|
|
|
126
131
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
127
132
|
|
|
@@ -153,6 +158,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
153
158
|
if (command === 'lifecycle') return await lifecycleMain(args,{cwd,env,fetchImpl,out,err});
|
|
154
159
|
if (command === 'admission') { const config=await loadConfig({cwd}); return await admissionMain(argv.slice(1),{client:createClient({...cloudConnection(config,env),fetchImpl}),out,err}); }
|
|
155
160
|
if (command === 'disavow') return await disavowMain(args,{cwd,env,fetchImpl,out});
|
|
161
|
+
if (command === 'chronology') return await chronologyMain(args,{cwd,env,fetchImpl,out});
|
|
156
162
|
if (command === 'index') return await indexMain(args,{cwd,env,fetchImpl,out});
|
|
157
163
|
if (command === 'setup') return await setupPlanMain(argv, { cwd, out });
|
|
158
164
|
if (command === 'tools' || command === 'describe' || command === 'schema' || command === 'call') return await commandsMain(argv, { cwd, out, err, env, fetchImpl });
|
|
@@ -305,7 +311,15 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
305
311
|
if (!name || !path) throw new ConfigError(`--observations-of expects NAME=FILE, got "${value}"`);
|
|
306
312
|
observationsOf[name] = path;
|
|
307
313
|
}
|
|
308
|
-
const
|
|
314
|
+
const statesOf = {};
|
|
315
|
+
for (const value of [].concat(args['state-of'] ?? [])) {
|
|
316
|
+
const separator = String(value).indexOf('=');
|
|
317
|
+
const name = String(value).slice(0, separator), path = String(value).slice(separator + 1);
|
|
318
|
+
if (separator < 1 || !path || !pairs.some(project => project.name === name)) throw new ConfigError('--state-of expects a declared NAME=FILE');
|
|
319
|
+
if (Object.hasOwn(statesOf, name)) throw new ConfigError(`Duplicate state input for ${name}`);
|
|
320
|
+
statesOf[name] = path;
|
|
321
|
+
}
|
|
322
|
+
const summary = await fleetSummary(pairs, { observationsOf, statesOf });
|
|
309
323
|
if (args.json) { out(JSON.stringify(summary, null, 1)); return 0; }
|
|
310
324
|
return renderFleet(summary, out);
|
|
311
325
|
}
|
|
@@ -391,6 +405,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
391
405
|
// so it is unchanged by a cosmetic edit to the template and changes the moment a figure does.
|
|
392
406
|
const asOf = args['as-of'] === true ? null : (args['as-of'] ?? null);
|
|
393
407
|
const dataset = freezeDataset(ledger.entries, observations.rows, {
|
|
408
|
+
activityState: await readState(config.paths.state),
|
|
394
409
|
asOf: asOf ?? new Date().toISOString().slice(0, 10),
|
|
395
410
|
title: args.title === true ? 'Backlink report' : (args.title ?? 'Backlink report'),
|
|
396
411
|
includeNotes: args['include-notes'] === true,
|
|
@@ -499,24 +514,29 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
499
514
|
}
|
|
500
515
|
|
|
501
516
|
if (command === 'status' || command === 'diff') {
|
|
502
|
-
const
|
|
503
|
-
|
|
517
|
+
const activityState = command === 'status' ? await readState(config.paths.state) : null;
|
|
518
|
+
const found = command === 'status' ? disagreements(ledger.entries, observations.rows, { state: activityState }) : transitions(observations.rows);
|
|
519
|
+
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;
|
|
520
|
+
if (args.json) { out(JSON.stringify(activity ? { disagreements: found, placements: activity } : found, null, 1)); return 0; }
|
|
504
521
|
reportProblems(ledger.problems, 'ledger line(s)', out);
|
|
505
522
|
if (command === 'diff') {
|
|
506
523
|
if (!found.length) out('no state changes recorded');
|
|
507
524
|
for (const change of found.slice(0, 100)) out(` ${change.at.slice(0, 10)} ${change.from} -> ${change.to} ${change.id}`);
|
|
508
525
|
return 0;
|
|
509
526
|
}
|
|
510
|
-
const groups = { appeared: 'appeared', lost: '
|
|
527
|
+
const groups = { appeared: 'appeared', lost: 'confirmed missing', suspected_missing: 'suspected missing', cannot_say: 'cannot say', never_checked: 'never checked' };
|
|
511
528
|
for (const [kind, label] of Object.entries(groups)) {
|
|
512
529
|
const rows = found.filter(item => item.kind === kind);
|
|
513
530
|
if (!rows.length) continue;
|
|
514
531
|
out(`\n${label} (${rows.length})`);
|
|
515
532
|
for (const item of rows.slice(0, 50)) {
|
|
516
533
|
out(` ${item.entry.id} ${item.entry.source}${item.row ? ` [${item.row.reason}]` : ''}`);
|
|
534
|
+
out(` Latest attempt: ${item.activity?.latest_attempt?.checked_at ?? 'not retained'} (${item.activity?.latest_attempt?.reason ?? 'unknown'})`);
|
|
535
|
+
out(` Last conclusive: ${item.activity?.last_successful_observation?.checked_at ?? 'not retained'}; last link verification: ${item.activity?.last_link_verification?.checked_at ?? 'not retained'}`);
|
|
517
536
|
}
|
|
518
537
|
}
|
|
519
538
|
if (!found.length) out('intent and observations agree on every entry');
|
|
539
|
+
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'}`);
|
|
520
540
|
return 0;
|
|
521
541
|
}
|
|
522
542
|
|
|
@@ -586,10 +606,11 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
586
606
|
}
|
|
587
607
|
|
|
588
608
|
if (command === 'check') {
|
|
609
|
+
if (ledger.problems.length) { reportProblems(ledger.problems, 'ledger line(s)', err); return 2; }
|
|
589
610
|
const state = await readState(config.paths.state);
|
|
590
611
|
const selected = selectEntries(ledger.entries, { filter: args.filter === true ? null : args.filter });
|
|
591
|
-
const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state));
|
|
592
|
-
if (!entries.length) { out(`nothing due (${plural(selected.length, 'entry', 'entries')} in scope; pass --all to recheck)`); return 0; }
|
|
612
|
+
const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state, selected));
|
|
613
|
+
if (!entries.length) { (args.json ? err : out)(`nothing due (${plural(selected.length, 'entry', 'entries')} in scope; pass --all to recheck)`); return 0; }
|
|
593
614
|
if (!args.json) err(`checking ${plural(entries.length, 'entry', 'entries')}…`);
|
|
594
615
|
const rows = await runCheck(entries, {
|
|
595
616
|
concurrency: Number(args.concurrency ?? config.defaults.concurrency ?? DEFAULTS.concurrency),
|
|
@@ -603,8 +624,8 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
603
624
|
await appendObservations(config.paths.observations, changed);
|
|
604
625
|
await writeState(config.paths.state, applyRun(state, rows));
|
|
605
626
|
const summary = summarize(entries, rows);
|
|
606
|
-
|
|
607
|
-
|
|
627
|
+
const exitCode = args['fail-on-unknown'] && summary.counts.unknown ? 1 : summary.exitCode;
|
|
628
|
+
if (args.json) { out(rows.map(row => JSON.stringify(row)).join('\n')); return exitCode; }
|
|
608
629
|
for (const { entry, row } of summary.appeared) out(`APPEARED ${entry.id} ${entry.source} (${row.occurrences} occurrence(s))`);
|
|
609
630
|
for (const { entry, row } of summary.failures) out(`ABSENT ${entry.id} ${entry.source} [${row.reason}]`);
|
|
610
631
|
out(`\n${JSON.stringify(summary.counts)}`);
|
|
@@ -612,8 +633,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
612
633
|
if (summary.counts.unknown) {
|
|
613
634
|
out(`${plural(summary.counts.unknown, 'page')} could not be concluded. An unknown is not a lost link; run \`agentlinkops status\` for the reasons.`);
|
|
614
635
|
}
|
|
615
|
-
|
|
616
|
-
return summary.exitCode;
|
|
636
|
+
return exitCode;
|
|
617
637
|
}
|
|
618
638
|
|
|
619
639
|
err(`unknown command: ${command}\n\n${USAGE}`);
|
|
@@ -624,6 +644,13 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
624
644
|
err(JSON.stringify({error: {code: error.code, status: error.status, next: 'Keep this project ledger and its history. Resolve authorized read access for the configured project, then retry agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.'}}));
|
|
625
645
|
return 2;
|
|
626
646
|
}
|
|
647
|
+
if (command === 'sync' && error instanceof CloudError && error.status === 0 &&
|
|
648
|
+
['CLOUD_TIMEOUT', 'CLOUD_NETWORK_ERROR'].includes(error.code)) {
|
|
649
|
+
// A local transport failure cannot establish whether a write committed.
|
|
650
|
+
// Print trusted recovery guidance, never attached exception/server text.
|
|
651
|
+
err(`agentlinkops: ${error.code}\nThe request outcome is unknown. Check the operation result before sending it again. Keep this project ledger and its history. Inspect accessible watches and their local references with agentlinkops sync --pull-only --recover-cursors. Do not broaden grants or replay remote writes.`);
|
|
652
|
+
return 2;
|
|
653
|
+
}
|
|
627
654
|
err(`agentlinkops: ${error?.message ?? error}`);
|
|
628
655
|
return 2;
|
|
629
656
|
} finally {
|
package/cli/mcp-client.js
CHANGED
|
@@ -62,7 +62,7 @@ function retryDelay(error, attempt, now) {
|
|
|
62
62
|
|
|
63
63
|
async function sdkClient({url, token, workspaceId, fetchImpl}) {
|
|
64
64
|
const {Client, StreamableHTTPClientTransport} = await import('@modelcontextprotocol/client');
|
|
65
|
-
const client = new Client({name: 'agentlinkops-cli', version: '0.6.
|
|
65
|
+
const client = new Client({name: 'agentlinkops-cli', version: '0.6.13'});
|
|
66
66
|
const destination = new URL(url);
|
|
67
67
|
const transport = new StreamableHTTPClientTransport(destination, {
|
|
68
68
|
authProvider: {token: async () => token},
|
|
@@ -71,7 +71,7 @@ async function sdkClient({url, token, workspaceId, fetchImpl}) {
|
|
|
71
71
|
if (target.href !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');
|
|
72
72
|
const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
73
73
|
if (workspaceId) headers.set('X-Workspace-ID', workspaceId);
|
|
74
|
-
headers.set('User-Agent', 'agentlinkops-cli/0.6.
|
|
74
|
+
headers.set('User-Agent', 'agentlinkops-cli/0.6.13');
|
|
75
75
|
const response = await fetchImpl(input, {...init, headers, redirect: 'error', signal: AbortSignal.timeout(30000)});
|
|
76
76
|
if (response.redirected || response.status >= 300 && response.status < 400
|
|
77
77
|
|| response.url && response.url !== destination.href) fail('OAUTH_TOKEN_DESTINATION_MISMATCH');
|
package/cli/mirror.js
CHANGED
|
File without changes
|
package/cli/mix.js
CHANGED
|
File without changes
|
package/cli/native-lock.js
CHANGED
|
File without changes
|
package/cli/oauth-credentials.js
CHANGED
|
File without changes
|
package/cli/oauth-flow.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// One identity-bound projection for local activity and retained observations. A change-only
|
|
2
|
+
// mirror is not a complete probe log, so local absence remains provisional. Hosted watch
|
|
3
|
+
// state is carried independently from the raw check; this module never confirms loss.
|
|
4
|
+
import { mirrorEvidenceReference } from '../src/evidence-reference.js';
|
|
5
|
+
|
|
6
|
+
const conclusive = new Set(['present', 'absent', 'source_unavailable']);
|
|
7
|
+
const watchStates = new Set(['present', 'suspected_missing', 'confirmed_missing', 'source_unavailable']);
|
|
8
|
+
export const validTime = value => typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
9
|
+
|
|
10
|
+
export function placementIdentity(value) {
|
|
11
|
+
const record = value?.result ?? value;
|
|
12
|
+
const source = record?.sourceUrl ?? record?.source_url ?? record?.source;
|
|
13
|
+
const target = record?.targetUrl ?? record?.target_url ?? record?.target;
|
|
14
|
+
const scope = record?.targetScope ?? record?.target_scope ?? record?.scope ?? 'exact';
|
|
15
|
+
return typeof source === 'string' && typeof target === 'string'
|
|
16
|
+
? JSON.stringify([source, target, scope]) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function attemptSummary(row, { retained = false } = {}) {
|
|
20
|
+
if (!validTime(row?.checked_at)) return null;
|
|
21
|
+
const cloud = row.source === 'cloud';
|
|
22
|
+
const rawState = cloud && conclusive.has(row.result?.checkState) ? row.result.checkState
|
|
23
|
+
: ['suspected_missing', 'confirmed_missing'].includes(row.state) ? 'absent' : row.state;
|
|
24
|
+
const method = row.result?.evidence?.method ?? null;
|
|
25
|
+
return {
|
|
26
|
+
checked_at: row.checked_at, placement: placementIdentity(row),
|
|
27
|
+
state: rawState, reason: row.reason ?? null, complete: row.complete === true,
|
|
28
|
+
origin: row.source ?? 'local',
|
|
29
|
+
watch_state: cloud && watchStates.has(row.result?.watchState) ? row.result.watchState
|
|
30
|
+
: ['suspected_missing', 'confirmed_missing'].includes(row.state) ? row.state : null,
|
|
31
|
+
link_signature: row.result?.linkSignature ?? null,
|
|
32
|
+
method, checker_version: row.checker_version ?? row.result?.evidence?.checkerVersion ?? null,
|
|
33
|
+
content_hash: row.result?.evidence?.sha256 ?? null,
|
|
34
|
+
evidence_reference: retained ? mirrorEvidenceReference({ entryId: row.id, checkedAt: row.checked_at, origin: row.source ?? 'local' }) : null,
|
|
35
|
+
retained,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const isConclusive = attempt => attempt?.complete === true && conclusive.has(attempt.state);
|
|
40
|
+
export const isLinkVerification = attempt => isConclusive(attempt) && ['present', 'absent'].includes(attempt.state);
|
|
41
|
+
|
|
42
|
+
// Equal timestamps never qualify another probe. Stable tie-breaking makes frozen exports
|
|
43
|
+
// independent of input ordering; an activity summary cannot replace a richer retained receipt.
|
|
44
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
45
|
+
function latest(items) {
|
|
46
|
+
return items.filter(item => item && validTime(item.checked_at)).sort((a, b) =>
|
|
47
|
+
Date.parse(b.checked_at) - Date.parse(a.checked_at)
|
|
48
|
+
|| Number(b.retained === true) - Number(a.retained === true)
|
|
49
|
+
|| compareText(JSON.stringify(a), JSON.stringify(b)))[0] ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function activityAttempt(record, identity) {
|
|
53
|
+
if (record?.last_placement !== identity) return null;
|
|
54
|
+
if (record.last_attempt?.placement === identity && record.last_attempt.checked_at === record.last_checked
|
|
55
|
+
&& validTime(record.last_attempt.checked_at)) return record.last_attempt;
|
|
56
|
+
// Legacy activity can prove its last conclusive time, but cannot invent a method/hash or
|
|
57
|
+
// reconstruct the overwritten successful check after a later unknown attempt.
|
|
58
|
+
if (!validTime(record.last_checked)) return null;
|
|
59
|
+
return { checked_at: record.last_checked, placement: identity, state: record.last_state ?? 'unknown',
|
|
60
|
+
reason: record.last_reason ?? null, complete: record.last_complete === true, origin: 'local',
|
|
61
|
+
watch_state: null, link_signature: record.last_signature ?? null, method: null,
|
|
62
|
+
checker_version: null, content_hash: null, evidence_reference: null, retained: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function projectPlacement(entry, observations, { state = null } = {}) {
|
|
66
|
+
const identity = placementIdentity(entry);
|
|
67
|
+
const history = identity ? observations.filter(row => row.id === entry.id && placementIdentity(row) === identity && validTime(row.checked_at)) : [];
|
|
68
|
+
const attempts = history.map(row => attemptSummary(row, { retained: true }));
|
|
69
|
+
const record = state?.entries?.[entry.id];
|
|
70
|
+
const locate = item => item?.retained === true && !attempts.some(receipt => receipt.checked_at === item.checked_at
|
|
71
|
+
&& receipt.origin === item.origin && receipt.content_hash === item.content_hash)
|
|
72
|
+
? { ...item, retained: false, evidence_reference: null } : item;
|
|
73
|
+
const activity = locate(identity ? activityAttempt(record, identity) : null);
|
|
74
|
+
const saved = key => locate(identity && record?.last_placement === identity && record[key]?.placement === identity ? record[key] : null);
|
|
75
|
+
const latestAttempt = latest([...attempts, activity]);
|
|
76
|
+
const savedSuccess = saved('last_successful_observation'), savedLink = saved('last_link_verification');
|
|
77
|
+
const lastSuccessful = latest([...attempts.filter(isConclusive), isConclusive(savedSuccess) ? savedSuccess : null, isConclusive(activity) ? activity : null]);
|
|
78
|
+
const lastLink = latest([...attempts.filter(isLinkVerification), isLinkVerification(savedLink) ? savedLink : null, isLinkVerification(activity) ? activity : null]);
|
|
79
|
+
const retained = latest(attempts);
|
|
80
|
+
const row = retained ? history.filter(item => JSON.stringify(attemptSummary(item, { retained: true })) === JSON.stringify(retained))
|
|
81
|
+
.sort((a, b) => compareText(JSON.stringify(a), JSON.stringify(b)))[0] ?? null : null;
|
|
82
|
+
const firstDates = attempts.filter(item => isConclusive(item) && item.state === 'present').map(item => item.checked_at);
|
|
83
|
+
const first = saved('first_present_observation');
|
|
84
|
+
if (isConclusive(first) && first.state === 'present' && validTime(first.checked_at)) firstDates.push(first.checked_at);
|
|
85
|
+
const firstPresent = firstDates.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? null;
|
|
86
|
+
const current = latestAttempt?.watch_state ?? lastSuccessful?.watch_state ?? (lastSuccessful?.state === 'absent' ? 'suspected_missing'
|
|
87
|
+
: lastSuccessful?.state ?? (latestAttempt ? 'unknown' : 'unchecked'));
|
|
88
|
+
return {
|
|
89
|
+
entry, row, placement: identity, current_state: current,
|
|
90
|
+
uncertain: !!latestAttempt && !isConclusive(latestAttempt),
|
|
91
|
+
latest_attempt: latestAttempt, last_successful_observation: lastSuccessful,
|
|
92
|
+
last_link_verification: lastLink, first_present: firstPresent,
|
|
93
|
+
evidence_observed_at: retained?.checked_at ?? null,
|
|
94
|
+
activity_available: activity !== null,
|
|
95
|
+
identity_status: history.length || activity ? 'matched' : observations.some(item => item.id === entry.id) || record ? 'not_matched' : 'not_observed',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function projectPlacements(entries, observations, options = {}) {
|
|
100
|
+
const byId = new Map();
|
|
101
|
+
for (const row of observations) {
|
|
102
|
+
if (!byId.has(row.id)) byId.set(row.id, []);
|
|
103
|
+
byId.get(row.id).push(row);
|
|
104
|
+
}
|
|
105
|
+
return entries.map(entry => projectPlacement(entry, byId.get(entry.id) ?? [], options));
|
|
106
|
+
}
|
package/cli/platforms.js
CHANGED
|
File without changes
|
|
File without changes
|
package/cli/receipts.js
CHANGED
|
File without changes
|