agentlinkops 0.6.1 → 0.6.3
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 +17 -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 +130 -0
- package/cli/agent-setup.js +1014 -101
- package/cli/agentlinkops.mjs +0 -0
- package/cli/args.js +0 -0
- package/cli/catalog.js +12837 -4694
- package/cli/check.js +0 -0
- package/cli/citation.js +181 -32
- package/cli/client.js +1 -1
- package/cli/commands.js +3 -3
- package/cli/config.js +2 -1
- 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 +54 -0
- package/cli/doctor.js +7 -8
- package/cli/env.js +0 -0
- package/cli/fleet.js +0 -0
- package/cli/gsc-browser.js +0 -0
- package/cli/import.js +0 -0
- package/cli/index-observations.js +89 -0
- package/cli/ledger.js +7 -1
- package/cli/lifecycle.js +40 -0
- package/cli/linktrail.mjs +0 -0
- package/cli/local-result.js +0 -0
- package/cli/main.js +99 -7
- package/cli/mirror.js +0 -0
- package/cli/mix.js +0 -0
- package/cli/platforms.js +0 -0
- package/cli/receipt-performance.js +0 -0
- package/cli/receipts.js +0 -0
- package/cli/report.js +79 -6
- package/cli/setup-plan.js +0 -0
- package/cli/skill.js +41 -4
- package/cli/state.js +0 -0
- package/cli/status.js +0 -0
- package/cli/sync.js +23 -1
- package/package.json +3 -2
- package/references/citation-panels.md +26 -0
- package/references/cli.md +0 -0
- package/references/disavow.md +0 -0
- package/references/discovery-handoff.md +32 -0
- package/references/recipes/catalog.json +148 -0
- package/references/recipes/command-notes.md +9 -0
- package/references/recipes/placement-reconciliation.md +41 -0
- package/references/recipes/qualified-campaign-handoff.md +56 -0
- package/references/recipes/site-context-brief.md +25 -0
- package/references/recipes/sourced-linkable-asset.md +25 -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/report.py +0 -0
- package/shared/admission-contract.js +16 -0
- package/shared/admission-report-contract.js +8 -0
- package/shared/agent-clients.js +0 -0
- package/shared/citation-evidence-contract.js +27 -0
- package/shared/disavow-contract.js +18 -0
- package/shared/index-observation-contract.js +14 -0
- package/shared/lifecycle-contract.js +22 -0
- package/shared/lifecycle-money.js +24 -0
- package/shared/observation-event.js +12 -1
- package/shared/opportunity-record.js +259 -0
- package/shared/response-contract.js +2 -0
- package/shared/search-connection-contract.js +0 -0
- package/skills/agentlinkops-assets/SKILL.md +2 -0
- package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
- package/skills/agentlinkops-campaigns/SKILL.md +5 -1
- package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/SKILL.md +8 -0
- package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
- package/skills/agentlinkops-connect/references/agentlinkops.md +9 -5
- package/skills/agentlinkops-crm/SKILL.md +2 -0
- package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
- package/skills/agentlinkops-discovery/SKILL.md +17 -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 +102 -21
- package/src/citations/alert.js +38 -13
- package/src/citations/browser/engine.js +145 -50
- 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 +3 -1
- 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 +1 -0
- package/src/citations/contract.js +55 -9
- package/src/citations/inventory.js +106 -0
- package/src/citations/login.js +122 -0
- package/src/citations/match.js +12 -0
- package/src/citations/panel-builder.js +44 -39
- package/src/citations/report.js +88 -21
- package/src/citations/runner.js +59 -31
- package/src/citations/stats.js +133 -61
- package/src/citations/sync.js +131 -0
- package/src/citations/watch.js +57 -77
- 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 +47 -8
- package/src/corpus/seeds.js +132 -19
- package/src/discovery/contract.js +0 -0
- package/src/discovery/import.js +0 -0
- package/src/evidence-reference.js +112 -0
- package/src/index-observation.js +161 -0
- package/src/locator.js +112 -0
- package/src/tool-views.js +6 -2
- package/src/verifier/fetch.js +42 -6
- 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/src/citations/browser/gen/core/providers/_shared/google.js +0 -4
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +0 -321
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +0 -251
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +0 -54
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +0 -21
- package/src/citations/browser/gen/lib/input/response/getText.js +0 -10
- package/src/citations/fixtures/dataforseo-aio.json +0 -82
package/src/citations/stats.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Fixed-epoch summaries plus explicitly qualified sequential inference.
|
|
2
2
|
//
|
|
3
|
-
// Wilson intervals
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// published data.
|
|
3
|
+
// Wilson intervals describe each precommitted fixed-n epoch. Supplemental
|
|
4
|
+
// confidence sequences and likelihood ratios have their own assumptions and
|
|
5
|
+
// multiplicity budgets; they do not authorize optional sample collection.
|
|
6
|
+
// Bayesian run-length outputs are exploratory model posteriors, not calibrated
|
|
7
|
+
// alarm probabilities or verified dates of a real-world change.
|
|
9
8
|
export const WILSON_Z = 1.959963985; // two-sided 95%
|
|
10
9
|
|
|
11
10
|
/** Wilson score interval for k successes in n rendered runs. null when n is 0. */
|
|
12
11
|
export function wilsonInterval(k, n, z = WILSON_Z) {
|
|
13
|
-
if (!
|
|
12
|
+
if (!validCounts(k, n) || n === 0 || !Number.isFinite(z) || z <= 0) return null;
|
|
14
13
|
const p = k / n;
|
|
15
14
|
const z2 = z * z;
|
|
16
15
|
const denom = 1 + z2 / n;
|
|
@@ -21,7 +20,7 @@ export function wilsonInterval(k, n, z = WILSON_Z) {
|
|
|
21
20
|
|
|
22
21
|
/** Laplace-smoothed rate. (k+1)/(n+2) never reports 0 or 1 from finite data. */
|
|
23
22
|
export function smoothedRate(k, n) {
|
|
24
|
-
if (n
|
|
23
|
+
if (!validCounts(k, n) || n === 0) return null;
|
|
25
24
|
return (k + 1) / (n + 2);
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -55,7 +54,8 @@ export function classifyEpoch(current, baseline, limits) {
|
|
|
55
54
|
export function tallyOutcomes(outcomes) {
|
|
56
55
|
let k = 0, n = 0, unknowns = 0, mentioned = 0;
|
|
57
56
|
for (const outcome of outcomes) {
|
|
58
|
-
if (outcome === 'unknown') { unknowns += 1; continue; }
|
|
57
|
+
if (outcome === 'unknown' || outcome === null) { unknowns += 1; continue; }
|
|
58
|
+
if (!['cited','verified','mentioned','not_cited'].includes(outcome)) throw new RangeError('Unknown citation outcome');
|
|
59
59
|
n += 1;
|
|
60
60
|
if (outcome === 'cited' || outcome === 'verified') k += 1;
|
|
61
61
|
if (outcome === 'mentioned') mentioned += 1;
|
|
@@ -68,70 +68,142 @@ export function tallyOutcomes(outcomes) {
|
|
|
68
68
|
* "lost"; it states both rates with their intervals and sample sizes.
|
|
69
69
|
*/
|
|
70
70
|
export function describeChange(current, baseline) {
|
|
71
|
+
if (current.rate === null || current.ci_low === null || current.ci_high === null) return 'insufficient data: citation rate unknown (no interpretable observations)';
|
|
71
72
|
const fmt = (e) => `${e.rate.toFixed(2)} (${e.ci_low.toFixed(2)}–${e.ci_high.toFixed(2)}, n=${e.n})`;
|
|
72
73
|
if (!baseline) return `first epoch: citation rate ${fmt(current)}`;
|
|
73
74
|
return `citation rate moved from ${fmt(baseline)} to ${fmt(current)} between epochs`;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
//
|
|
77
|
+
// Sequential contracts and derivations are recorded in DP-0045/statistics-spec.md.
|
|
78
|
+
function validCounts(k, n) { return Number.isSafeInteger(k) && Number.isSafeInteger(n) && n >= 0 && k >= 0 && k <= n; }
|
|
79
|
+
function probability(value, name) { if (!Number.isFinite(value) || value <= 0 || value >= 1) throw new RangeError(`${name} must lie strictly between zero and one`); }
|
|
80
|
+
function multiplicity(alpha, comparisons) {
|
|
81
|
+
probability(alpha, 'alpha');
|
|
82
|
+
if (!Number.isSafeInteger(comparisons) || comparisons < 1) throw new RangeError('comparisons must be a positive preregistered count');
|
|
83
|
+
return alpha / comparisons;
|
|
84
|
+
}
|
|
85
|
+
function binary(value) {
|
|
86
|
+
if (value === null || value === 'unknown') return null;
|
|
87
|
+
if (value === 1 || value === 'cited' || value === 'verified') return 1;
|
|
88
|
+
if (value === 0 || value === 'not_cited' || value === 'mentioned') return 0;
|
|
89
|
+
throw new RangeError('Observations must be binary citation outcomes or explicit unknowns');
|
|
90
|
+
}
|
|
91
|
+
function boundedInterval(k, n, error) {
|
|
92
|
+
const radius = Math.sqrt(Math.log(2 / error) / (2 * n));
|
|
93
|
+
return [Math.max(0, k / n - radius), Math.min(1, k / n + radius)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Anytime interval via Hoeffding plus alpha/[m*n*(n+1)] spending.
|
|
97
|
+
* Independent Bernoulli observations with a constant mean are sufficient.
|
|
98
|
+
* Unknown exclusion additionally assumes missingness does not select outcomes.
|
|
99
|
+
* Union bounds cover every observed n and all m preregistered comparisons.
|
|
100
|
+
* This conservative construction is not the variance-adaptive confseq algorithm.
|
|
101
|
+
*/
|
|
102
|
+
export function confidenceSequence(observations, { alpha = 0.05, comparisons = 1 } = {}) {
|
|
103
|
+
const perCell = multiplicity(alpha, comparisons);
|
|
104
|
+
let k = 0, n = 0, unknowns = 0, lower = 0, upper = 1;
|
|
105
|
+
return observations.map(value => {
|
|
106
|
+
const x = binary(value);
|
|
107
|
+
if (x === null) unknowns += 1;
|
|
108
|
+
else {
|
|
109
|
+
n += 1; k += x;
|
|
110
|
+
const current = boundedInterval(k, n, perCell / (n * (n + 1)));
|
|
111
|
+
lower = Math.max(lower, current[0]); upper = Math.min(upper, current[1]);
|
|
112
|
+
}
|
|
113
|
+
return { k, n, unknowns, interval: n === 0 || lower > upper ? null : [lower, upper],
|
|
114
|
+
status: n === 0 ? 'insufficient_data' : lower > upper ? 'model_conflict' : 'available',
|
|
115
|
+
method: 'hoeffding-alpha-spending', alpha, comparisons };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Conservative fixed-n two-epoch difference interval. Both means receive half
|
|
120
|
+
* the per-comparison alpha. Repeated testing of many epoch pairs requires the
|
|
121
|
+
* total comparison count to be allocated in advance; no perpetual-alert claim.
|
|
122
|
+
*/
|
|
123
|
+
export function compareEpochs(current, baseline, { alpha = 0.05, comparisons = 1 } = {}) {
|
|
124
|
+
const error = multiplicity(alpha, comparisons) / 2;
|
|
125
|
+
if (!current || !baseline || !validCounts(current.k,current.n) || !validCounts(baseline.k,baseline.n) || current.n === 0 || baseline.n === 0)
|
|
126
|
+
return { verdict: 'insufficient_data', interval: null, alpha, comparisons, method: 'fixed-n-hoeffding-difference' };
|
|
127
|
+
const now = boundedInterval(current.k,current.n,error), prior = boundedInterval(baseline.k,baseline.n,error);
|
|
128
|
+
const interval = [Math.max(-1,now[0]-prior[1]),Math.min(1,now[1]-prior[0])];
|
|
129
|
+
return { verdict: interval[1] < 0 ? 'declined' : interval[0] > 0 ? 'grown' : 'not_distinguishable', interval, alpha, comparisons, method: 'fixed-n-hoeffding-difference' };
|
|
130
|
+
}
|
|
77
131
|
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
132
|
+
/** Simple-hypothesis sequential likelihood ratio with conservative Ville
|
|
133
|
+
* boundaries. p0/p1 must be selected before inspecting these observations;
|
|
134
|
+
* inserting an estimated prior-epoch rate does not retain the stated guarantees.
|
|
135
|
+
* A lower boundary supports p0 against p1; it does not establish stability.
|
|
136
|
+
*/
|
|
83
137
|
export function sprtDecline(k, n, baselineRate, declineRate = baselineRate * 0.5, alpha = 0.05, beta = 0.10) {
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const lower = Math.log(beta / (1 - alpha));
|
|
91
|
-
// Positive LLR favors H1 (the rate has declined); negative favors H0 (still at baseline).
|
|
92
|
-
if (llr >= upper) return { decision: 'decline', evidence: Number(llr.toFixed(4)) };
|
|
93
|
-
if (llr <= lower) return { decision: 'stable', evidence: Number(llr.toFixed(4)) };
|
|
94
|
-
return { decision: 'continue', evidence: Number(llr.toFixed(4)) };
|
|
138
|
+
if (!validCounts(k,n)) throw new RangeError('SPRT requires valid integer counts');
|
|
139
|
+
probability(baselineRate,'baselineRate'); probability(declineRate,'declineRate'); probability(alpha,'alpha'); probability(beta,'beta');
|
|
140
|
+
if (declineRate >= baselineRate) throw new RangeError('declineRate must be below the prespecified baselineRate');
|
|
141
|
+
const evidence = k * Math.log(declineRate / baselineRate) + (n-k) * Math.log((1-declineRate)/(1-baselineRate));
|
|
142
|
+
return { decision: evidence >= Math.log(1/alpha) ? 'decline' : evidence <= Math.log(beta) ? 'baseline_supported' : 'continue', evidence,
|
|
143
|
+
upper: Math.log(1/alpha), lower: Math.log(beta), method: 'simple-hypothesis-likelihood-ratio' };
|
|
95
144
|
}
|
|
96
145
|
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
export function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let
|
|
110
|
-
for
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
// Penalize the split by the hazard rate
|
|
120
|
-
const score = ll1 + ll2 + Math.log(1 - hazard) * (n - split - 1) + Math.log(hazard);
|
|
121
|
-
if (score > bestScore) { bestScore = score; bestSplit = split; }
|
|
146
|
+
/** Exact Beta-Bernoulli BOCD filtering with constant hazard. The posterior is
|
|
147
|
+
* conditional on this piecewise-iid model and the chosen prior/hazard. It is not
|
|
148
|
+
* a confidence sequence, an FDR-controlled detector or ground-truth dating.
|
|
149
|
+
* Indices refer to the supplied observation array, preserving unknown gaps.
|
|
150
|
+
*/
|
|
151
|
+
export function changepointPosterior(observations, { hazard = 0.01, priorAlpha = 1, priorBeta = 1, minSegment = 3, posteriorThreshold = 0.8, minEffect = 0.3 } = {}) {
|
|
152
|
+
probability(hazard,'hazard'); probability(posteriorThreshold,'posteriorThreshold');
|
|
153
|
+
if (![priorAlpha,priorBeta].every(x=>Number.isFinite(x)&&x>0) || !Number.isInteger(minSegment) || minSegment<2 || !Number.isFinite(minEffect) || minEffect<0 || minEffect>1)
|
|
154
|
+
throw new RangeError('Invalid changepoint prior or selection parameters');
|
|
155
|
+
const known = [];
|
|
156
|
+
observations.forEach((value,index) => { const x=binary(value); if(x!==null)known.push({x,index}); });
|
|
157
|
+
if(known.length>2000)throw new RangeError('Exact BOCD is bounded to 2000 known observations; select a disclosed analysis window');
|
|
158
|
+
let posterior=[1], successes=[0], counts=[0];
|
|
159
|
+
for(const {x} of known){
|
|
160
|
+
const next=new Array(posterior.length+1).fill(0);
|
|
161
|
+
for(let r=0;r<posterior.length;r++){
|
|
162
|
+
const predictive=(x?priorAlpha+successes[r]:priorBeta+counts[r]-successes[r])/(priorAlpha+priorBeta+counts[r]);
|
|
163
|
+
const joint=posterior[r]*predictive;
|
|
164
|
+
next[0]+=joint*hazard;next[r+1]=joint*(1-hazard);
|
|
165
|
+
}
|
|
166
|
+
const sum=next.reduce((a,b)=>a+b,0);posterior=next.map(value=>value/sum);
|
|
167
|
+
successes=[0,...successes.map(value=>value+x)];counts=[0,...counts.map(value=>value+1)];
|
|
122
168
|
}
|
|
123
|
-
|
|
169
|
+
const n=known.length, candidates=[];
|
|
170
|
+
for(let r=minSegment;r<=n-minSegment;r++)candidates.push({split:n-r,index:known[n-r].index,probability:posterior[r]});
|
|
171
|
+
const changeProbability=candidates.reduce((sum,row)=>sum+row.probability,0);
|
|
172
|
+
const best=candidates.reduce((best,row)=>!best||row.probability>best.probability?row:best,null);
|
|
173
|
+
let candidate=null;
|
|
174
|
+
if(best && changeProbability>=posteriorThreshold){
|
|
175
|
+
const before=known.slice(0,best.split),after=known.slice(best.split);
|
|
176
|
+
const delta=smoothedRate(after.reduce((sum,row)=>sum+row.x,0),after.length)-smoothedRate(before.reduce((sum,row)=>sum+row.x,0),before.length);
|
|
177
|
+
if(Math.abs(delta)>=minEffect && best.probability > posterior[n])candidate={index:best.index,known_split:best.split,delta};
|
|
178
|
+
}
|
|
179
|
+
// A conditional posterior location interval, never a frequentist date CI.
|
|
180
|
+
let locationInterval=null;
|
|
181
|
+
if(candidate && changeProbability>0){
|
|
182
|
+
let sum=0,low=null,high=null;
|
|
183
|
+
for(const row of candidates.slice().sort((a,b)=>a.index-b.index)){
|
|
184
|
+
sum+=row.probability/changeProbability;
|
|
185
|
+
if(low===null && sum>=0.025)low=row.index;
|
|
186
|
+
if(high===null && sum>=0.975)high=row.index;
|
|
187
|
+
}
|
|
188
|
+
locationInterval=[low,high??candidates[0].index];
|
|
189
|
+
}
|
|
190
|
+
return { method:'beta-bernoulli-bocd', interpretation:'exploratory', n, unknowns:observations.length-n, hazard,
|
|
191
|
+
run_length_posterior:posterior, candidate, location_interval:locationInterval, interior_change_probability:changeProbability,
|
|
192
|
+
no_change_probability:posterior[n]??1 };
|
|
124
193
|
}
|
|
125
194
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
195
|
+
/** Compatibility helper: an exploratory boundary index, never an exact date. */
|
|
196
|
+
export function detectChangepoint(observations, options = {}) { return changepointPosterior(observations,options).candidate?.index ?? null; }
|
|
197
|
+
|
|
198
|
+
/** Descriptive Laplace rates over original-position windows. Unknowns occupy
|
|
199
|
+
* time positions but contribute neither success nor denominator. */
|
|
129
200
|
export function rollingRate(observations, windowSize = 5) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
201
|
+
if(!Number.isSafeInteger(windowSize)||windowSize<1)throw new RangeError('windowSize must be a positive integer');
|
|
202
|
+
const values=observations.map(binary),rates=[];let k=0,n=0;
|
|
203
|
+
for(let i=0;i<values.length;i++){
|
|
204
|
+
if(values[i]!==null){n++;k+=values[i];}
|
|
205
|
+
if(i>=windowSize && values[i-windowSize]!==null){n--;k-=values[i-windowSize];}
|
|
206
|
+
rates.push(smoothedRate(k,n));
|
|
135
207
|
}
|
|
136
208
|
return rates;
|
|
137
209
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `agentlinkops citation sync` — push local epoch rows to the hosted API
|
|
2
|
+
// so the console Citations view shows real data without the hosted scheduler.
|
|
3
|
+
// Reads the local citations-epochs.jsonl and POSTs each row to the hosted
|
|
4
|
+
// citation_epochs endpoint via the existing REST client.
|
|
5
|
+
//
|
|
6
|
+
// The sync is one-way (local → hosted) and idempotent: the hosted side
|
|
7
|
+
// deduplicates by epoch_id + cell_id. The local ledger is the source of
|
|
8
|
+
// truth; the hosted view is a projection.
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export async function syncEpochs({ dir, api, workspaceId, projectId }) {
|
|
13
|
+
if (!api || !workspaceId || !projectId) {
|
|
14
|
+
throw new Error('citation sync needs api, workspaceId and projectId (connect first: agentlinkops connect)');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const epochsPath = join(resolve(dir), 'citations-epochs.jsonl');
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
text = await readFile(epochsPath, 'utf8');
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code !== 'ENOENT') throw error;
|
|
23
|
+
return { synced: 0, total: 0, errors: 0, note: 'no local epochs found' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rows = text.split('\n').filter((l) => l.trim()).map((l) => {
|
|
27
|
+
return JSON.parse(l);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (rows.length === 0) return { synced: 0, total: 0, errors: 0, note: 'epoch file is empty' };
|
|
31
|
+
|
|
32
|
+
const normalizedRates = rows.filter(row => Number.isInteger(row.n) && Number.isInteger(row.k) && row.n >= 0 && row.k >= 0 && row.k <= row.n && row.rate !== (row.n ? row.k / row.n : null)).length;
|
|
33
|
+
// Legacy smoothed rates are projected from retained counts; never rewrite the local ledger.
|
|
34
|
+
// Send the epoch rows in batches (the API accepts up to 100 per call)
|
|
35
|
+
let synced = 0;
|
|
36
|
+
let errors = 0;
|
|
37
|
+
for (let i = 0; i < rows.length; i += 100) {
|
|
38
|
+
const batch = rows.slice(i, i + 100).map((row) => ({
|
|
39
|
+
workspace_id: workspaceId,
|
|
40
|
+
project_id: projectId,
|
|
41
|
+
epoch_id: row.epoch_id,
|
|
42
|
+
cell_id: row.cell_id,
|
|
43
|
+
k: row.k,
|
|
44
|
+
n: row.n,
|
|
45
|
+
unknowns: row.unknowns ?? 0,
|
|
46
|
+
mentioned: row.mentioned ?? 0,
|
|
47
|
+
rate: row.n === 0 ? null : row.k / row.n,
|
|
48
|
+
ci_low: row.n === 0 ? null : row.ci_low,
|
|
49
|
+
ci_high: row.n === 0 ? null : row.ci_high,
|
|
50
|
+
classification: row.classification,
|
|
51
|
+
baseline_epoch_id: row.baseline?.epoch_id ?? null,
|
|
52
|
+
baseline_rate: row.baseline?.rate ?? null,
|
|
53
|
+
baseline_ci_low: row.baseline?.ci_low ?? null,
|
|
54
|
+
baseline_ci_high: row.baseline?.ci_high ?? null,
|
|
55
|
+
spent_estimate_usd: row.spent_estimate_usd ?? 0,
|
|
56
|
+
at: row.at,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await api('/v1/commands/sync_citation_epochs', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: { epochs: batch, projectId },
|
|
63
|
+
});
|
|
64
|
+
synced += batch.length;
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
errors += batch.length;
|
|
67
|
+
console.error(`sync batch failed: ${cause.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { synced, errors, total: rows.length, ...(normalizedRates ? { normalized_rates: normalizedRates, normalization: 'Hosted rates projected as k/n (null for n=0); local ledger unchanged.' } : {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Explicit opt-in: raw retained answer text leaves the local ledger only here.
|
|
75
|
+
export async function syncEvidence({ dir, api, projectId, cliVersion }) {
|
|
76
|
+
const { lstat, realpath } = await import('node:fs/promises');
|
|
77
|
+
const { createHash } = await import('node:crypto');
|
|
78
|
+
const { evidenceEnvelopeSchema } = await import('./contract.js');
|
|
79
|
+
const { CITATION_EVIDENCE_LIMITS: limits, citationEvidenceRecordInput } = await import('../../shared/citation-evidence-contract.js');
|
|
80
|
+
if (!api || !projectId || !cliVersion) throw new Error('Evidence sync requires a connected project and CLI version');
|
|
81
|
+
const root = await realpath(resolve(dir));
|
|
82
|
+
let text;
|
|
83
|
+
try { text = await readFile(join(root, 'citations-observations.jsonl'), 'utf8'); }
|
|
84
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; return { synced: 0, total: 0, skipped: 0, errors: 0, reasons: {} }; }
|
|
85
|
+
const rows = text.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
86
|
+
const records = []; const reasons = {}; const seen = new Set();
|
|
87
|
+
const skip = reason => { reasons[reason] = (reasons[reason] ?? 0) + 1; };
|
|
88
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
89
|
+
async function safeFile(path) {
|
|
90
|
+
const actual = await realpath(path);
|
|
91
|
+
if (!actual.startsWith(root + '/') || !(await lstat(path)).isFile()) throw new Error('Evidence path is not a regular ledger file');
|
|
92
|
+
return actual;
|
|
93
|
+
}
|
|
94
|
+
// Validate the complete local selection before sending any evidence batch.
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(row.epoch_id ?? '') || ['.', '..'].includes(row.epoch_id) || !/^[a-f0-9]{64}$/.test(row.evidence_sha256 ?? '')) throw new Error('Malformed local evidence reference');
|
|
97
|
+
const path = join(root, 'citations/evidence', row.epoch_id, `${row.evidence_sha256}.json`);
|
|
98
|
+
if (seen.has(path)) continue; seen.add(path);
|
|
99
|
+
let file;
|
|
100
|
+
try { file = await safeFile(path); } catch (error) { if (error.code === 'ENOENT') { skip('missing_envelope'); continue; } throw error; }
|
|
101
|
+
if ((await lstat(file)).size > limits.envelopeBytes) throw new Error('Evidence envelope exceeds the upload limit');
|
|
102
|
+
const envelopeJson = await readFile(file, 'utf8');
|
|
103
|
+
const envelope = evidenceEnvelopeSchema.parse(JSON.parse(envelopeJson));
|
|
104
|
+
if (envelope.epoch_id !== row.epoch_id || envelope.cell_id !== row.cell_id || envelope.run_index !== row.run_index) throw new Error('Evidence identity does not match its observation');
|
|
105
|
+
if (envelope.engine_identity.startsWith('google-aio:') && !envelope.provenance) { skip('legacy_aio_without_provenance'); continue; }
|
|
106
|
+
const record = { envelopeJson, sha256: hash(envelopeJson), cliVersion, localEvidenceSha256: row.evidence_sha256 };
|
|
107
|
+
if (envelope.screenshot_file) {
|
|
108
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(envelope.screenshot_file) || ['.', '..'].includes(envelope.screenshot_file)) throw new Error('Unsafe screenshot reference');
|
|
109
|
+
record.screenshot = { reference: envelope.screenshot_file, sha256: null };
|
|
110
|
+
try {
|
|
111
|
+
const image = await safeFile(join(root, 'citations/evidence', row.epoch_id, envelope.screenshot_file));
|
|
112
|
+
if ((await lstat(image)).size <= 10 * 1024 * 1024) record.screenshot.sha256 = hash(await readFile(image));
|
|
113
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
114
|
+
}
|
|
115
|
+
records.push(citationEvidenceRecordInput.parse(record));
|
|
116
|
+
}
|
|
117
|
+
const batches = []; let batch = [];
|
|
118
|
+
const bytes = records => Buffer.byteLength(JSON.stringify({ projectId, records }));
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
if (bytes([record]) > limits.requestBytes) throw new Error('Encoded evidence exceeds the request limit');
|
|
121
|
+
if (batch.length && (batch.length >= limits.records || bytes([...batch, record]) > limits.requestBytes)) { batches.push(batch); batch = []; }
|
|
122
|
+
batch.push(record);
|
|
123
|
+
}
|
|
124
|
+
if (batch.length) batches.push(batch);
|
|
125
|
+
let synced = 0; let errors = 0;
|
|
126
|
+
for (const records of batches) {
|
|
127
|
+
try { await api('/v1/commands/ingest_citation_evidence', { method: 'POST', body: { projectId, records } }); synced += records.length; }
|
|
128
|
+
catch { errors += records.length; }
|
|
129
|
+
}
|
|
130
|
+
return { synced, total: seen.size, skipped: Object.values(reasons).reduce((a, b) => a + b, 0), errors, reasons };
|
|
131
|
+
}
|
package/src/citations/watch.js
CHANGED
|
@@ -1,85 +1,65 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// into a monitoring product — the buyer sets it up once and gets weekly
|
|
4
|
-
// evidence in their inbox.
|
|
5
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
-
import { join, resolve, basename } from 'node:path';
|
|
1
|
+
// Local scheduling is explicit: no crontab is changed until register/remove is called.
|
|
2
|
+
import { resolve, dirname, basename, join } from 'node:path';
|
|
7
3
|
import { execFile } from 'node:child_process';
|
|
8
|
-
import {
|
|
4
|
+
import { validateAlertWebhook } from './alert.js';
|
|
9
5
|
|
|
10
|
-
const
|
|
6
|
+
const CADENCES = { hourly: '17 * * * *', daily: '17 9 * * *', weekly: '17 9 * * 1', monthly: '17 9 1 * *' };
|
|
7
|
+
const PREFIX = '# agentlinkops:citation-watch:';
|
|
8
|
+
const quote = value => "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// deterministic so the alert hook can find the latest artifact.
|
|
24
|
-
const command = [
|
|
25
|
-
`cd ${join(absolute, '..')}`,
|
|
26
|
-
`&& ${agentlinkopsBin} citation run ${absolute}`,
|
|
27
|
-
`&& ${agentlinkopsBin} citation report ${absolute}`,
|
|
28
|
-
// The alert hook reads the latest epoch rows; if any cell declined,
|
|
29
|
-
// the report is flagged (delivery is a follow-up: email or webhook).
|
|
30
|
-
`&& ${agentlinkopsBin} citation alert ${absolute}`,
|
|
31
|
-
].join(' ');
|
|
32
|
-
|
|
33
|
-
// Cron line: m h dom mon dow command — weekly on Monday at 09:17 UTC,
|
|
34
|
-
// daily at 09:17, hourly at minute 17 (off the :00 mark per convention).
|
|
35
|
-
const cronSpec = cadence === 'weekly' ? '17 9 * * 1'
|
|
36
|
-
: cadence === 'daily' ? '17 9 * * *'
|
|
37
|
-
: cadence === 'hourly' ? '17 * * * *'
|
|
38
|
-
: '17 9 1 * *'; // monthly
|
|
39
|
-
|
|
40
|
-
const marker = `# agentlinkops:citation-watch:${panelName}`;
|
|
41
|
-
const cronLine = `${cronSpec} ${command} 2>&1 | logger -t agentlinkops-citation ${marker}`;
|
|
42
|
-
|
|
43
|
-
// Read the existing crontab, replace our entry if present, append otherwise.
|
|
44
|
-
const existing = await new Promise((r) => {
|
|
45
|
-
execFile('crontab', ['-l'], (err, stdout) => r(err ? '' : stdout));
|
|
46
|
-
});
|
|
47
|
-
const lines = existing.split('\n').filter((l) => !l.includes(marker));
|
|
48
|
-
lines.push(cronLine);
|
|
49
|
-
await new Promise((r, j) => {
|
|
50
|
-
execFile('crontab', ['-'], { input: lines.join('\n') + '\n' }, (err) => err ? j(err) : r());
|
|
10
|
+
function run(args, input, execFileImpl) {
|
|
11
|
+
return new Promise((resolveResult, reject) => {
|
|
12
|
+
const child = execFileImpl('crontab', args, (error, stdout, stderr) => {
|
|
13
|
+
if (!error) return resolveResult(stdout ?? '');
|
|
14
|
+
if (args[0] === '-l' && error.code === 1 && /^no crontab for [^\r\n]+\s*$/i.test((stderr ?? '').trim())) return resolveResult('');
|
|
15
|
+
reject(new Error('Unable to ' + (args[0] === '-l' ? 'read' : 'write') + ' crontab', { cause: error }));
|
|
16
|
+
});
|
|
17
|
+
if (input !== undefined) {
|
|
18
|
+
child.stdin.on('error', reject);
|
|
19
|
+
child.stdin.end(input);
|
|
20
|
+
}
|
|
51
21
|
});
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
cadence,
|
|
55
|
-
cronSpec,
|
|
56
|
-
panelPath: absolute,
|
|
57
|
-
marker,
|
|
58
|
-
next: cadence === 'weekly' ? 'next Monday 09:17 UTC' : `every ${CADENCE_HOURS[cadence]}h at :17`,
|
|
59
|
-
};
|
|
60
22
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
execFile('crontab', ['-l'], (err, stdout) => r(err ? '' : stdout));
|
|
65
|
-
});
|
|
66
|
-
return existing.split('\n')
|
|
67
|
-
.filter((l) => l.includes('# agentlinkops:citation-watch:'))
|
|
68
|
-
.map((l) => {
|
|
69
|
-
const marker = l.match(/agentlinkops:citation-watch:(\S+)/);
|
|
70
|
-
const cronPart = l.split(/\s+/).slice(0, 5).join(' ');
|
|
71
|
-
return { panel: marker?.[1] ?? 'unknown', cron: cronPart };
|
|
72
|
-
});
|
|
23
|
+
function markerName(value) {
|
|
24
|
+
if (!value || /[\r\n\0]/.test(value)) throw new Error('Invalid citation watch name');
|
|
25
|
+
return encodeURIComponent(value);
|
|
73
26
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
27
|
+
function replace(existing, marker, line) {
|
|
28
|
+
const lines = existing.split('\n').filter(value => value && !value.endsWith(marker));
|
|
29
|
+
if (line) lines.push(line);
|
|
30
|
+
return lines.join('\n') + (lines.length ? '\n' : '');
|
|
31
|
+
}
|
|
32
|
+
export async function registerWatch({ panelPath, cadence = 'weekly', dir, out, webhook, execFileImpl = execFile, binary = process.argv[1], nodeBinary = process.execPath }) {
|
|
33
|
+
if (!CADENCES[cadence]) throw new Error(`unknown cadence: ${cadence}`);
|
|
34
|
+
if (webhook) validateAlertWebhook(webhook);
|
|
35
|
+
const absolute = resolve(panelPath);
|
|
36
|
+
const ledgerDir = resolve(dir ?? join(dirname(absolute), '.agentlinkops'));
|
|
37
|
+
const reportPath = resolve(out ?? join(ledgerDir, 'citations', 'report.html'));
|
|
38
|
+
const marker = PREFIX + markerName(basename(absolute, '.json'));
|
|
39
|
+
for (const value of [absolute, ledgerDir, reportPath, binary, nodeBinary]) {
|
|
40
|
+
if (!value || /[\r\n\0]/.test(value)) throw new Error('Invalid citation watch path');
|
|
41
|
+
}
|
|
42
|
+
const cli = `${quote(nodeBinary)} ${quote(resolve(binary))}`;
|
|
43
|
+
const delivery = webhook ? ` --webhook ${quote(webhook)} --report ${quote(reportPath)}` : '';
|
|
44
|
+
const common = `${quote(absolute)} --dir ${quote(ledgerDir)}`;
|
|
45
|
+
// Exit 1 means an actionable decline, so it must still produce its report and alert.
|
|
46
|
+
const command = `cd ${quote(dirname(absolute))} && { ${cli} citation run ${common}; status=$?; if [ "$status" -le 1 ]; then ${cli} citation report ${common} --out ${quote(reportPath)} && ${cli} citation alert ${common}${delivery}; else exit "$status"; fi; }`;
|
|
47
|
+
// Cron interprets percent even inside shell quotes.
|
|
48
|
+
const cronLine = `${CADENCES[cadence]} ${command.replace(/%/g, '\\%')} 2>&1 | logger -t agentlinkops-citation ${marker}`;
|
|
49
|
+
const existing = await run(['-l'], undefined, execFileImpl);
|
|
50
|
+
await run(['-'], replace(existing, marker, cronLine), execFileImpl);
|
|
51
|
+
return { cadence, cronSpec: CADENCES[cadence], panelPath: absolute, marker, next: 'according to the crontab host timezone' };
|
|
52
|
+
}
|
|
53
|
+
export async function listWatches({ execFileImpl = execFile } = {}) {
|
|
54
|
+
const existing = await run(['-l'], undefined, execFileImpl);
|
|
55
|
+
return existing.split('\n').filter(line => line.includes(PREFIX)).map(line => ({
|
|
56
|
+
panel: decodeURIComponent(line.slice(line.lastIndexOf(PREFIX) + PREFIX.length)),
|
|
57
|
+
cron: line.trim().split(/\s+/).slice(0, 5).join(' '),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
export async function removeWatch(panelName, { execFileImpl = execFile } = {}) {
|
|
61
|
+
const marker = PREFIX + markerName(panelName);
|
|
62
|
+
const existing = await run(['-l'], undefined, execFileImpl);
|
|
63
|
+
await run(['-'], replace(existing, marker), execFileImpl);
|
|
84
64
|
return { removed: panelName };
|
|
85
65
|
}
|
|
File without changes
|
|
File without changes
|
package/src/context/ga4.js
CHANGED
|
File without changes
|
package/src/context/gsc.js
CHANGED
|
File without changes
|
|
File without changes
|
package/src/context/util.js
CHANGED
|
File without changes
|
|
File without changes
|