agentlinkops 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/CHANGELOG.md +12 -0
- package/LICENSE +0 -0
- package/NOTICE +0 -0
- package/README.md +0 -0
- package/bin/agentlinkops.mjs +0 -0
- package/bin/linktrail.mjs +0 -0
- package/cli/adapters/csv.js +0 -0
- package/cli/adapters/sqlite-crm.js +0 -0
- package/cli/adapters/suppliers.js +0 -0
- package/cli/admission.js +0 -0
- package/cli/agent-setup.js +0 -0
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +4435 -175
- package/cli/check.js +0 -0
- package/cli/chronology.js +39 -0
- package/cli/citation.js +0 -0
- package/cli/client.js +1 -1
- package/cli/cloud-error.js +0 -0
- package/cli/commands.js +0 -0
- package/cli/config.js +0 -0
- package/cli/connection.js +0 -0
- package/cli/context-mcp.js +0 -0
- package/cli/context.js +0 -0
- package/cli/delivery.js +0 -0
- package/cli/disavow.js +0 -0
- package/cli/doctor.js +0 -0
- package/cli/env.js +0 -0
- package/cli/fleet.js +30 -19
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/index-observations.js +0 -0
- package/cli/index-submissions.js +0 -0
- package/cli/ledger.js +0 -0
- package/cli/lifecycle.js +0 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +34 -8
- package/cli/mcp-client.js +2 -2
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/native-lock.js +0 -0
- package/cli/oauth-credentials.js +0 -0
- package/cli/oauth-flow.js +0 -0
- package/cli/placement-projection.js +106 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +51 -19
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +0 -0
- package/cli/state.js +62 -29
- package/cli/status.js +14 -18
- package/cli/sync-lock.js +0 -0
- package/cli/sync.js +22 -3
- package/package.json +1 -1
- package/references/citation-panels.md +0 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +0 -0
- package/references/local-crm-draft-handoff.md +0 -0
- package/references/portable-ledger-handoff.md +0 -0
- package/references/recipes/catalog.json +0 -0
- package/references/recipes/command-notes.md +0 -0
- package/references/recipes/placement-reconciliation.md +0 -0
- package/references/recipes/qualified-campaign-handoff.md +0 -0
- package/references/recipes/site-context-brief.md +0 -0
- package/references/recipes/sourced-linkable-asset.md +0 -0
- package/references/reports.md +0 -0
- package/scripts/agentlinkops.py +0 -0
- package/scripts/disavow.py +0 -0
- package/scripts/linktrail.py +0 -0
- package/scripts/migrations/001_crm.sql +0 -0
- package/scripts/migrations/002_cloud_events.sql +0 -0
- package/scripts/migrations/003_disavow.sql +0 -0
- package/scripts/migrations/004_placement_costs.sql +0 -0
- package/scripts/outreach-crm-draft.py +0 -0
- package/scripts/portable-handoff.py +0 -0
- package/scripts/report.py +0 -0
- package/shared/admission-contract.js +0 -0
- package/shared/admission-report-contract.js +0 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +0 -0
- package/shared/disavow-contract.js +0 -0
- package/shared/exec-grammar.js +0 -0
- package/shared/index-observation-contract.js +2 -2
- package/shared/lifecycle-contract.js +0 -0
- package/shared/lifecycle-money.js +0 -0
- package/shared/monitoring-chronology-export.js +57 -0
- package/shared/monitoring-read-contract.js +41 -0
- package/shared/observation-event.js +0 -0
- package/shared/opportunity-record.js +0 -0
- package/shared/response-contract.js +3 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +0 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +0 -0
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +0 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +4 -3
- package/skills/agentlinkops-crm/SKILL.md +0 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +0 -0
- package/skills/content-defingerprinting/SKILL.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
- package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
- package/skills/internal-linking-optimizer/SKILL.md +0 -0
- package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
- package/skills/pre-publish-review/SKILL.md +0 -0
- package/skills/source-cited-content-builder/SKILL.md +0 -0
- package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
- package/skills/source-cited-humanizer/SKILL.md +0 -0
- package/src/citations/adapter.js +0 -0
- package/src/citations/alert.js +0 -0
- package/src/citations/browser/chatgpt-citations.js +0 -0
- package/src/citations/browser/engine.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +0 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +0 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +0 -0
- package/src/citations/browser/gen/core/providers/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +0 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +0 -0
- package/src/citations/browser/gen/lib/browser/display.js +0 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +0 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +0 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +0 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +0 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +0 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +0 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +0 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +0 -0
- package/src/citations/browser/gen/utils/agent-constants.js +0 -0
- package/src/citations/browser/providers.js +0 -0
- package/src/citations/browser/proxy-admission.js +0 -0
- package/src/citations/browser/shims/errors.js +0 -0
- package/src/citations/browser/shims/types.js +0 -0
- package/src/citations/browser/shims/utils.js +0 -0
- package/src/citations/contract.js +0 -0
- package/src/citations/egress-receipts.js +0 -0
- package/src/citations/evidence-integrity.js +0 -0
- package/src/citations/inventory.js +0 -0
- package/src/citations/login.js +0 -0
- package/src/citations/match.js +0 -0
- package/src/citations/panel-builder.js +0 -0
- package/src/citations/report.js +0 -0
- package/src/citations/runner.js +0 -0
- package/src/citations/stats.js +0 -0
- package/src/citations/sync.js +0 -0
- package/src/citations/watch.js +0 -0
- package/src/competitors/contract.js +0 -0
- package/src/competitors/domain-mix.js +0 -0
- package/src/context/ga4.js +0 -0
- package/src/context/gsc.js +0 -0
- package/src/context/site-profile.js +0 -0
- package/src/context/util.js +0 -0
- package/src/context-tool-registry.js +0 -0
- package/src/corpus/acquire.js +0 -0
- package/src/corpus/seeds.js +0 -0
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/evidence-reference.js +0 -0
- package/src/index-observation.js +0 -0
- package/src/locator.js +0 -0
- package/src/response-shaping.js +0 -0
- package/src/tool-views.js +5 -4
- package/src/verifier/fetch.js +0 -0
- package/src/verifier/index.js +0 -0
- package/src/verifier/observation-kind.js +0 -0
- package/src/verifier/parser.js +0 -0
- package/src/verifier/readiness.js +0 -0
- package/src/verifier/render-evidence.js +0 -0
- package/src/verifier/robots.js +0 -0
- package/src/verifier/transition.js +0 -0
- package/src/verifier/url.js +0 -0
- package/templates/SITE.template.md +0 -0
- package/templates/campaigns.v1.json +0 -0
package/cli/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
|
File without changes
|
|
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]
|
|
@@ -122,7 +126,7 @@ evidence · 2 usage, configuration or ledger error. Unknown is nonfailing by def
|
|
|
122
126
|
Names: \`linktrail\` still runs this CLI, LINKTRAIL_* variables are still read, and an existing
|
|
123
127
|
.linktrail/ directory is still used, each with a one-line notice, until the DP-0029 cutover.`;
|
|
124
128
|
|
|
125
|
-
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:[]});
|
|
126
130
|
|
|
127
131
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
128
132
|
|
|
@@ -154,6 +158,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
154
158
|
if (command === 'lifecycle') return await lifecycleMain(args,{cwd,env,fetchImpl,out,err});
|
|
155
159
|
if (command === 'admission') { const config=await loadConfig({cwd}); return await admissionMain(argv.slice(1),{client:createClient({...cloudConnection(config,env),fetchImpl}),out,err}); }
|
|
156
160
|
if (command === 'disavow') return await disavowMain(args,{cwd,env,fetchImpl,out});
|
|
161
|
+
if (command === 'chronology') return await chronologyMain(args,{cwd,env,fetchImpl,out});
|
|
157
162
|
if (command === 'index') return await indexMain(args,{cwd,env,fetchImpl,out});
|
|
158
163
|
if (command === 'setup') return await setupPlanMain(argv, { cwd, out });
|
|
159
164
|
if (command === 'tools' || command === 'describe' || command === 'schema' || command === 'call') return await commandsMain(argv, { cwd, out, err, env, fetchImpl });
|
|
@@ -306,7 +311,15 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
306
311
|
if (!name || !path) throw new ConfigError(`--observations-of expects NAME=FILE, got "${value}"`);
|
|
307
312
|
observationsOf[name] = path;
|
|
308
313
|
}
|
|
309
|
-
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 });
|
|
310
323
|
if (args.json) { out(JSON.stringify(summary, null, 1)); return 0; }
|
|
311
324
|
return renderFleet(summary, out);
|
|
312
325
|
}
|
|
@@ -392,6 +405,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
392
405
|
// so it is unchanged by a cosmetic edit to the template and changes the moment a figure does.
|
|
393
406
|
const asOf = args['as-of'] === true ? null : (args['as-of'] ?? null);
|
|
394
407
|
const dataset = freezeDataset(ledger.entries, observations.rows, {
|
|
408
|
+
activityState: await readState(config.paths.state),
|
|
395
409
|
asOf: asOf ?? new Date().toISOString().slice(0, 10),
|
|
396
410
|
title: args.title === true ? 'Backlink report' : (args.title ?? 'Backlink report'),
|
|
397
411
|
includeNotes: args['include-notes'] === true,
|
|
@@ -500,24 +514,29 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
500
514
|
}
|
|
501
515
|
|
|
502
516
|
if (command === 'status' || command === 'diff') {
|
|
503
|
-
const
|
|
504
|
-
|
|
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; }
|
|
505
521
|
reportProblems(ledger.problems, 'ledger line(s)', out);
|
|
506
522
|
if (command === 'diff') {
|
|
507
523
|
if (!found.length) out('no state changes recorded');
|
|
508
524
|
for (const change of found.slice(0, 100)) out(` ${change.at.slice(0, 10)} ${change.from} -> ${change.to} ${change.id}`);
|
|
509
525
|
return 0;
|
|
510
526
|
}
|
|
511
|
-
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' };
|
|
512
528
|
for (const [kind, label] of Object.entries(groups)) {
|
|
513
529
|
const rows = found.filter(item => item.kind === kind);
|
|
514
530
|
if (!rows.length) continue;
|
|
515
531
|
out(`\n${label} (${rows.length})`);
|
|
516
532
|
for (const item of rows.slice(0, 50)) {
|
|
517
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'}`);
|
|
518
536
|
}
|
|
519
537
|
}
|
|
520
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'}`);
|
|
521
540
|
return 0;
|
|
522
541
|
}
|
|
523
542
|
|
|
@@ -590,7 +609,7 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
590
609
|
if (ledger.problems.length) { reportProblems(ledger.problems, 'ledger line(s)', err); return 2; }
|
|
591
610
|
const state = await readState(config.paths.state);
|
|
592
611
|
const selected = selectEntries(ledger.entries, { filter: args.filter === true ? null : args.filter });
|
|
593
|
-
const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state));
|
|
612
|
+
const entries = args.all ? selected : dueEntries(selected, lastCheckedMap(state, selected));
|
|
594
613
|
if (!entries.length) { (args.json ? err : out)(`nothing due (${plural(selected.length, 'entry', 'entries')} in scope; pass --all to recheck)`); return 0; }
|
|
595
614
|
if (!args.json) err(`checking ${plural(entries.length, 'entry', 'entries')}…`);
|
|
596
615
|
const rows = await runCheck(entries, {
|
|
@@ -625,6 +644,13 @@ export async function main(argv = process.argv.slice(2), { cwd = process.cwd(),
|
|
|
625
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.'}}));
|
|
626
645
|
return 2;
|
|
627
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
|
+
}
|
|
628
654
|
err(`agentlinkops: ${error?.message ?? error}`);
|
|
629
655
|
return 2;
|
|
630
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
|