@prenta/core 5.5.0 → 5.7.0
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/CHANGELOG.md +31 -0
- package/dist/ai/governance.d.ts +6 -0
- package/dist/ai/governance.d.ts.map +1 -1
- package/dist/ai/governance.js +19 -0
- package/dist/ai/governance.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +2 -0
- package/dist/api/handlers.js.map +1 -1
- package/dist/api/routes/redirects.d.ts.map +1 -1
- package/dist/api/routes/redirects.js +89 -93
- package/dist/api/routes/redirects.js.map +1 -1
- package/dist/api/routes/seo-autopilot.d.ts +12 -0
- package/dist/api/routes/seo-autopilot.d.ts.map +1 -0
- package/dist/api/routes/seo-autopilot.js +257 -0
- package/dist/api/routes/seo-autopilot.js.map +1 -0
- package/dist/api/routes/seo.d.ts.map +1 -1
- package/dist/api/routes/seo.js +195 -360
- package/dist/api/routes/seo.js.map +1 -1
- package/dist/db/model-types.d.ts +26 -0
- package/dist/db/model-types.d.ts.map +1 -1
- package/dist/generated/browser.d.ts +8 -0
- package/dist/generated/client.d.ts +8 -0
- package/dist/generated/internal/class.d.ts +11 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.d.ts +120 -1
- package/dist/generated/internal/prismaNamespace.js +42 -0
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +44 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +42 -0
- package/dist/generated/models/SeoAutoApply.d.ts +1495 -0
- package/dist/generated/models/SeoAutoApply.js +1 -0
- package/dist/generated/models.d.ts +1 -0
- package/dist/redirects/recovery.d.ts +15 -9
- package/dist/redirects/recovery.d.ts.map +1 -1
- package/dist/redirects/recovery.js +15 -61
- package/dist/redirects/recovery.js.map +1 -1
- package/dist/redirects/suggestion-accept.d.ts +29 -0
- package/dist/redirects/suggestion-accept.d.ts.map +1 -0
- package/dist/redirects/suggestion-accept.js +108 -0
- package/dist/redirects/suggestion-accept.js.map +1 -0
- package/dist/seo/audit-runner.d.ts.map +1 -1
- package/dist/seo/audit-runner.js +0 -2
- package/dist/seo/audit-runner.js.map +1 -1
- package/dist/seo/autopilot-gate.d.ts +101 -0
- package/dist/seo/autopilot-gate.d.ts.map +1 -0
- package/dist/seo/autopilot-gate.js +272 -0
- package/dist/seo/autopilot-gate.js.map +1 -0
- package/dist/seo/autopilot-ledger.d.ts +74 -0
- package/dist/seo/autopilot-ledger.d.ts.map +1 -0
- package/dist/seo/autopilot-ledger.js +118 -0
- package/dist/seo/autopilot-ledger.js.map +1 -0
- package/dist/seo/autopilot-outcome.d.ts +93 -0
- package/dist/seo/autopilot-outcome.d.ts.map +1 -0
- package/dist/seo/autopilot-outcome.js +206 -0
- package/dist/seo/autopilot-outcome.js.map +1 -0
- package/dist/seo/autopilot-policy.d.ts +169 -0
- package/dist/seo/autopilot-policy.d.ts.map +1 -0
- package/dist/seo/autopilot-policy.js +267 -0
- package/dist/seo/autopilot-policy.js.map +1 -0
- package/dist/seo/autopilot-revert.d.ts +40 -0
- package/dist/seo/autopilot-revert.d.ts.map +1 -0
- package/dist/seo/autopilot-revert.js +155 -0
- package/dist/seo/autopilot-revert.js.map +1 -0
- package/dist/seo/autopilot.d.ts +91 -0
- package/dist/seo/autopilot.d.ts.map +1 -0
- package/dist/seo/autopilot.js +1060 -0
- package/dist/seo/autopilot.js.map +1 -0
- package/dist/seo/config-store.d.ts +35 -2
- package/dist/seo/config-store.d.ts.map +1 -1
- package/dist/seo/config-store.js +58 -1
- package/dist/seo/config-store.js.map +1 -1
- package/dist/seo/deep-equal.d.ts +12 -0
- package/dist/seo/deep-equal.d.ts.map +1 -0
- package/dist/seo/deep-equal.js +42 -0
- package/dist/seo/deep-equal.js.map +1 -0
- package/dist/seo/digest.d.ts +27 -0
- package/dist/seo/digest.d.ts.map +1 -1
- package/dist/seo/digest.js +77 -1
- package/dist/seo/digest.js.map +1 -1
- package/dist/seo/index.d.ts +20 -2
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +12 -1
- package/dist/seo/index.js.map +1 -1
- package/dist/seo/issue-fix-ai.d.ts +40 -1
- package/dist/seo/issue-fix-ai.d.ts.map +1 -1
- package/dist/seo/issue-fix-ai.js +53 -1
- package/dist/seo/issue-fix-ai.js.map +1 -1
- package/dist/seo/issue-fix-apply.d.ts +104 -0
- package/dist/seo/issue-fix-apply.d.ts.map +1 -0
- package/dist/seo/issue-fix-apply.js +406 -0
- package/dist/seo/issue-fix-apply.js.map +1 -0
- package/dist/seo/issue-fix.d.ts +9 -0
- package/dist/seo/issue-fix.d.ts.map +1 -1
- package/dist/seo/issue-fix.js +1 -0
- package/dist/seo/issue-fix.js.map +1 -1
- package/dist/seo/proposals-bulk-apply.d.ts +52 -0
- package/dist/seo/proposals-bulk-apply.d.ts.map +1 -0
- package/dist/seo/proposals-bulk-apply.js +183 -0
- package/dist/seo/proposals-bulk-apply.js.map +1 -0
- package/dist/seo/proposals-generate.d.ts +45 -0
- package/dist/seo/proposals-generate.d.ts.map +1 -0
- package/dist/seo/proposals-generate.js +144 -0
- package/dist/seo/proposals-generate.js.map +1 -0
- package/dist/seo/proposals.d.ts +125 -0
- package/dist/seo/proposals.d.ts.map +1 -0
- package/dist/seo/proposals.js +270 -0
- package/dist/seo/proposals.js.map +1 -0
- package/package.json +1 -1
- package/prisma/migrations/0017_seo_auto_applies/migration.sql +35 -0
- package/prisma/schema.prisma +43 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { AUTOPILOT_OUTCOME_LAG_DAYS, isAutopilotIssueType, } from './autopilot-policy.js';
|
|
2
|
+
import { startOfDayUTC } from './autopilot-ledger.js';
|
|
3
|
+
import { revertSeoAutoApply } from './autopilot-revert.js';
|
|
4
|
+
/** Upper bound on ledger rows judged per cron tick. */
|
|
5
|
+
export const OUTCOME_MAX_PER_TICK = 50;
|
|
6
|
+
/** A regression whose revert keeps throwing is requeued at most this many times. */
|
|
7
|
+
export const OUTCOME_MAX_REVERT_ATTEMPTS = 3;
|
|
8
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
/** Absorbs float error in `prior × (1 − dropRatio)` (e.g. 50 × 0.2 = 9.999…). */
|
|
10
|
+
const RATIO_EPSILON = 1e-9;
|
|
11
|
+
/**
|
|
12
|
+
* Pure judgement. A zero `prior` (no ratio to take) or one below the floor is
|
|
13
|
+
* not enough signal; otherwise a fall to or below `prior × (1 − dropRatio)` is
|
|
14
|
+
* a regression.
|
|
15
|
+
*/
|
|
16
|
+
export function evaluateOutcome(prior, after, watch) {
|
|
17
|
+
if (prior <= 0 || prior < watch.minPriorClicks)
|
|
18
|
+
return 'insufficient_data';
|
|
19
|
+
if (after <= prior * (1 - watch.dropRatio) + RATIO_EPSILON)
|
|
20
|
+
return 'regressed';
|
|
21
|
+
return 'held';
|
|
22
|
+
}
|
|
23
|
+
async function sumClicks(db, path, from, to) {
|
|
24
|
+
const agg = await db.searchPerfDaily.aggregate({
|
|
25
|
+
where: { path, date: { gte: from, lte: to } },
|
|
26
|
+
_sum: { clicks: true },
|
|
27
|
+
});
|
|
28
|
+
return agg._sum.clicks ?? 0;
|
|
29
|
+
}
|
|
30
|
+
function isoDay(d) {
|
|
31
|
+
return d.toISOString().slice(0, 10);
|
|
32
|
+
}
|
|
33
|
+
/** `revertAttempts` recorded on the row by a previous tick, if any. */
|
|
34
|
+
function priorRevertAttempts(metrics) {
|
|
35
|
+
if (metrics && typeof metrics === 'object' && !Array.isArray(metrics)) {
|
|
36
|
+
const v = metrics.revertAttempts;
|
|
37
|
+
if (typeof v === 'number' && Number.isInteger(v) && v > 0)
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Judge every due, still-pending issue fix (oldest first, capped per tick).
|
|
44
|
+
* Due = applied at least `days + AUTOPILOT_OUTCOME_LAG_DAYS` ago *and* GSC data
|
|
45
|
+
* synced through the end of the after window. Prior window: `days` days ending
|
|
46
|
+
* the day before apply. After window: `days` days starting the day after apply.
|
|
47
|
+
*
|
|
48
|
+
* A regression reverts the row with reason `outcome_regression` (never
|
|
49
|
+
* forced — a human edit since is a conflict) and requests a pause of that
|
|
50
|
+
* issue type via `pausedTypes`; the caller persists the pause on the policy.
|
|
51
|
+
* A revert that throws is logged, counted in `revertErrors`, and the row is
|
|
52
|
+
* put back to `pending` (with `outcomeMetrics.revertAttempts`) so the next
|
|
53
|
+
* tick retries — up to `OUTCOME_MAX_REVERT_ATTEMPTS`, after which it stays
|
|
54
|
+
* `regressed` for a manual revert. The tick continues with the next row.
|
|
55
|
+
*
|
|
56
|
+
* The pause is decided (and `onPause` fired) before the revert is attempted,
|
|
57
|
+
* so a revert that throws — or a process killed mid-revert — never loses the
|
|
58
|
+
* breaker. `opts.deadline` bounds the wall clock: rows not reached stay
|
|
59
|
+
* `pending` and are judged next tick.
|
|
60
|
+
*/
|
|
61
|
+
export async function runOutcomeWatch(db, policy, opts) {
|
|
62
|
+
const result = {
|
|
63
|
+
evaluated: 0,
|
|
64
|
+
held: 0,
|
|
65
|
+
regressed: 0,
|
|
66
|
+
reverted: 0,
|
|
67
|
+
insufficient: 0,
|
|
68
|
+
conflicts: 0,
|
|
69
|
+
revertErrors: 0,
|
|
70
|
+
notReady: 0,
|
|
71
|
+
deadlineHit: false,
|
|
72
|
+
pausedTypes: {},
|
|
73
|
+
};
|
|
74
|
+
const watch = policy.outcomeWatch;
|
|
75
|
+
if (!watch.enabled)
|
|
76
|
+
return result;
|
|
77
|
+
const clock = opts.clock ?? Date.now;
|
|
78
|
+
const pastDeadline = () => opts.deadline !== undefined && clock() >= opts.deadline;
|
|
79
|
+
const newest = await db.searchPerfDaily.findFirst({
|
|
80
|
+
orderBy: { date: 'desc' },
|
|
81
|
+
select: { date: true },
|
|
82
|
+
});
|
|
83
|
+
if (!newest)
|
|
84
|
+
return result;
|
|
85
|
+
const revert = opts.revert ?? revertSeoAutoApply;
|
|
86
|
+
const dueBefore = new Date(opts.now.getTime() - (watch.days + AUTOPILOT_OUTCOME_LAG_DAYS) * DAY_MS);
|
|
87
|
+
const rows = await db.seoAutoApply.findMany({
|
|
88
|
+
where: {
|
|
89
|
+
kind: 'issue-fix',
|
|
90
|
+
status: 'applied',
|
|
91
|
+
outcome: 'pending',
|
|
92
|
+
appliedAt: { lte: dueBefore },
|
|
93
|
+
},
|
|
94
|
+
orderBy: { appliedAt: 'asc' },
|
|
95
|
+
take: OUTCOME_MAX_PER_TICK,
|
|
96
|
+
});
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
if (pastDeadline()) {
|
|
99
|
+
result.deadlineHit = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
// Rows are recorded `skipped` when path is null, but tolerate a stray one.
|
|
103
|
+
if (!row.path)
|
|
104
|
+
continue;
|
|
105
|
+
const day = startOfDayUTC(row.appliedAt);
|
|
106
|
+
const priorFrom = new Date(day.getTime() - watch.days * DAY_MS);
|
|
107
|
+
const priorTo = new Date(day.getTime() - DAY_MS);
|
|
108
|
+
const afterFrom = new Date(day.getTime() + DAY_MS);
|
|
109
|
+
const afterTo = new Date(day.getTime() + watch.days * DAY_MS);
|
|
110
|
+
if (afterTo > newest.date) {
|
|
111
|
+
result.notReady++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const [prior, after] = await Promise.all([
|
|
115
|
+
sumClicks(db, row.path, priorFrom, priorTo),
|
|
116
|
+
sumClicks(db, row.path, afterFrom, afterTo),
|
|
117
|
+
]);
|
|
118
|
+
const judgement = evaluateOutcome(prior, after, watch);
|
|
119
|
+
result.evaluated++;
|
|
120
|
+
const dropRatio = prior > 0 ? (prior - after) / prior : 0;
|
|
121
|
+
const dropPct = Math.round(dropRatio * 100);
|
|
122
|
+
const previousAttempts = priorRevertAttempts(row.outcomeMetrics);
|
|
123
|
+
const metrics = {
|
|
124
|
+
priorClicks: prior,
|
|
125
|
+
afterClicks: after,
|
|
126
|
+
dropRatio,
|
|
127
|
+
windowDays: watch.days,
|
|
128
|
+
checkedRange: {
|
|
129
|
+
priorFrom: isoDay(priorFrom),
|
|
130
|
+
priorTo: isoDay(priorTo),
|
|
131
|
+
afterFrom: isoDay(afterFrom),
|
|
132
|
+
afterTo: isoDay(afterTo),
|
|
133
|
+
},
|
|
134
|
+
...(previousAttempts > 0 ? { revertAttempts: previousAttempts } : {}),
|
|
135
|
+
};
|
|
136
|
+
await db.seoAutoApply.update({
|
|
137
|
+
where: { id: row.id },
|
|
138
|
+
data: { outcome: judgement, outcomeCheckedAt: opts.now, outcomeMetrics: metrics },
|
|
139
|
+
});
|
|
140
|
+
switch (judgement) {
|
|
141
|
+
case 'insufficient_data':
|
|
142
|
+
result.insufficient++;
|
|
143
|
+
break;
|
|
144
|
+
case 'held':
|
|
145
|
+
result.held++;
|
|
146
|
+
break;
|
|
147
|
+
case 'regressed': {
|
|
148
|
+
result.regressed++;
|
|
149
|
+
// The regression itself is the signal — pause the type before (and whether
|
|
150
|
+
// or not) the revert lands, and let the caller persist it right away.
|
|
151
|
+
if (isAutopilotIssueType(row.issueType)) {
|
|
152
|
+
const reason = `Paused ${isoDay(opts.now)}: ${row.path} lost ${dropPct}% clicks after an autopilot change`;
|
|
153
|
+
result.pausedTypes[row.issueType] = reason;
|
|
154
|
+
if (opts.onPause) {
|
|
155
|
+
try {
|
|
156
|
+
await opts.onPause(row.issueType, reason);
|
|
157
|
+
}
|
|
158
|
+
catch (pauseErr) {
|
|
159
|
+
console.warn('[seo-autopilot] outcome pause callback failed', {
|
|
160
|
+
type: row.issueType,
|
|
161
|
+
error: String(pauseErr),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const outcome = await revert(db, row.id, {
|
|
168
|
+
actor: opts.actor,
|
|
169
|
+
reason: 'outcome_regression',
|
|
170
|
+
force: false,
|
|
171
|
+
});
|
|
172
|
+
if (outcome.status === 'reverted')
|
|
173
|
+
result.reverted++;
|
|
174
|
+
else if (outcome.status === 'conflict')
|
|
175
|
+
result.conflicts++;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
result.revertErrors++;
|
|
179
|
+
console.warn('[seo-autopilot] outcome revert failed', { id: row.id, error: String(err) });
|
|
180
|
+
const attempts = previousAttempts + 1;
|
|
181
|
+
// Requeue so the next tick re-judges and retries; give up after the cap.
|
|
182
|
+
const outcome = attempts < OUTCOME_MAX_REVERT_ATTEMPTS ? 'pending' : 'regressed';
|
|
183
|
+
try {
|
|
184
|
+
await db.seoAutoApply.update({
|
|
185
|
+
where: { id: row.id },
|
|
186
|
+
data: { outcome, outcomeMetrics: { ...metrics, revertAttempts: attempts } },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (requeueErr) {
|
|
190
|
+
console.warn('[seo-autopilot] outcome requeue failed', {
|
|
191
|
+
id: row.id,
|
|
192
|
+
error: String(requeueErr),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
default: {
|
|
199
|
+
const exhaustive = judgement;
|
|
200
|
+
return exhaustive;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=autopilot-outcome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autopilot-outcome.js","sourceRoot":"","sources":["../../src/seo/autopilot-outcome.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO Autopilot policy — the per-issue-type opt-in, thresholds, caps, and
|
|
3
|
+
* pause state that govern unattended apply. Stored on
|
|
4
|
+
* `SeoModuleSettings.autopilot` (spec §1). The AI layer's governance flags
|
|
5
|
+
* (`approvalRequired` / `allowBulkApply`) remain a separate veto.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AUTOPILOT_METADATA_TYPES: readonly ["missing-meta-title", "meta-title-length", "duplicate-meta-title", "missing-meta-description", "meta-description-length", "duplicate-meta-description", "missing-canonical", "accidental-noindex", "missing-structured-data"];
|
|
8
|
+
export declare const AUTOPILOT_ISSUE_TYPES: readonly ["missing-meta-title", "meta-title-length", "duplicate-meta-title", "missing-meta-description", "meta-description-length", "duplicate-meta-description", "missing-canonical", "accidental-noindex", "missing-structured-data", "redirect"];
|
|
9
|
+
export type AutopilotIssueType = (typeof AUTOPILOT_ISSUE_TYPES)[number];
|
|
10
|
+
export declare function isAutopilotIssueType(value: unknown): value is AutopilotIssueType;
|
|
11
|
+
export type AutopilotPauseReason = 'outcome_regression' | 'owner_unavailable' | 'manual';
|
|
12
|
+
export interface AutopilotOutcomeWatch {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
days: number;
|
|
15
|
+
dropRatio: number;
|
|
16
|
+
minPriorClicks: number;
|
|
17
|
+
}
|
|
18
|
+
export interface SeoAutopilotPolicy {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
enabledById: string | null;
|
|
21
|
+
pausedAt: string | null;
|
|
22
|
+
pausedReason: AutopilotPauseReason | null;
|
|
23
|
+
types: Partial<Record<AutopilotIssueType, boolean>>;
|
|
24
|
+
pausedTypes: Partial<Record<AutopilotIssueType, string>>;
|
|
25
|
+
minBrandAlignment: number;
|
|
26
|
+
redirectMinConfidence: number;
|
|
27
|
+
maxPerRun: number;
|
|
28
|
+
maxPerDay: number;
|
|
29
|
+
verifyFailuresPerRun: number;
|
|
30
|
+
outcomeWatch: AutopilotOutcomeWatch;
|
|
31
|
+
}
|
|
32
|
+
export type GateReason = 'policy_disabled' | 'policy_paused' | 'type_not_enabled' | 'type_paused' | 'governance_approval_required' | 'governance_bulk_not_allowed' | 'cap_per_run' | 'cap_per_day' | 'stale' | 'dismissed' | 'cooldown' | 'brand_below_threshold' | 'brand_unscored' | 'banned_term' | 'validator_failed' | 'duplicate_value' | 'redirect_low_confidence' | 'redirect_manual_exists' | 'plan_denied';
|
|
33
|
+
export interface AutopilotRunSummary {
|
|
34
|
+
startedAt: string;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
applied: number;
|
|
37
|
+
verifyFailed: number;
|
|
38
|
+
/** Outcome-watch reverts this tick. */
|
|
39
|
+
reverted: number;
|
|
40
|
+
gated: Partial<Record<GateReason, number>>;
|
|
41
|
+
/**
|
|
42
|
+
* applyIssueFix / acceptRedirectSuggestion statuses other than applied /
|
|
43
|
+
* verify-failed (`stale`, `not-found`, `not-fixable`, `not-allowed`,
|
|
44
|
+
* `plan-required`, `invalid`, `still-live`), plus run-level counters:
|
|
45
|
+
* - `error` — applyIssueFix / acceptRedirectSuggestion threw and no write landed
|
|
46
|
+
* - `gate_mismatch` — an apply landed a value other than the gated patch and was
|
|
47
|
+
* restored (also counted in `verifyFailed`)
|
|
48
|
+
* - `after_unreadable` — the document could not be re-read after an apply, so the
|
|
49
|
+
* written value could not be verified against the gated patch; treated as drift:
|
|
50
|
+
* restored and ledgered `verify_failed` with `after` = the gated patch (also
|
|
51
|
+
* counted in `verifyFailed`, never in `gate_mismatch`)
|
|
52
|
+
* - `outcome_revert_error` — outcome-watch reverts that threw (row requeued)
|
|
53
|
+
* - `outcome_watch_error` — the outcome watch itself failed (run continued)
|
|
54
|
+
*/
|
|
55
|
+
applyOutcomes: Record<string, number>;
|
|
56
|
+
/**
|
|
57
|
+
* `haltedReason`: the generator's own reason (`budget_exhausted`, …) or
|
|
58
|
+
* `generate_failed` when the generate phase threw. Never a raw error message —
|
|
59
|
+
* the summary is served to every authenticated role; the message is logged.
|
|
60
|
+
*/
|
|
61
|
+
generated: {
|
|
62
|
+
generated: number;
|
|
63
|
+
skipped: number;
|
|
64
|
+
failed: number;
|
|
65
|
+
haltedReason: string | null;
|
|
66
|
+
};
|
|
67
|
+
halted: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `disabled` | `paused` | `owner_unavailable` | `already_running` (another
|
|
70
|
+
* tick holds the run lease; nothing was written) | `verify_failures`
|
|
71
|
+
* (breaker) | `budget_exhausted` | `ledger_write_failed` (a write could not
|
|
72
|
+
* be ledgered: it was undone best-effort and the run stopped) |
|
|
73
|
+
* `apply_failed` (the apply phase threw; the message is logged, never stored)
|
|
74
|
+
* | `run_failed` (an unexpected escape from the phases; re-thrown to the
|
|
75
|
+
* caller after the summary is persisted).
|
|
76
|
+
*/
|
|
77
|
+
haltedReason: string | null;
|
|
78
|
+
}
|
|
79
|
+
export declare const DEFAULT_AUTOPILOT_POLICY: SeoAutopilotPolicy;
|
|
80
|
+
export declare const AUTOPILOT_LIMITS: {
|
|
81
|
+
readonly maxPerRun: {
|
|
82
|
+
readonly min: 1;
|
|
83
|
+
readonly max: 25;
|
|
84
|
+
};
|
|
85
|
+
readonly maxPerDay: {
|
|
86
|
+
readonly min: 1;
|
|
87
|
+
readonly max: 100;
|
|
88
|
+
};
|
|
89
|
+
readonly verifyFailuresPerRun: {
|
|
90
|
+
readonly min: 1;
|
|
91
|
+
readonly max: 10;
|
|
92
|
+
};
|
|
93
|
+
readonly minBrandAlignment: {
|
|
94
|
+
readonly min: 0;
|
|
95
|
+
readonly max: 100;
|
|
96
|
+
};
|
|
97
|
+
readonly redirectMinConfidence: {
|
|
98
|
+
readonly min: 50;
|
|
99
|
+
readonly max: 100;
|
|
100
|
+
};
|
|
101
|
+
readonly outcomeDays: {
|
|
102
|
+
readonly min: 7;
|
|
103
|
+
readonly max: 28;
|
|
104
|
+
};
|
|
105
|
+
readonly dropRatio: {
|
|
106
|
+
readonly min: 0.2;
|
|
107
|
+
readonly max: 0.8;
|
|
108
|
+
};
|
|
109
|
+
readonly minPriorClicks: {
|
|
110
|
+
readonly min: 0;
|
|
111
|
+
readonly max: 100000;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/** A reverted change keeps its issue off autopilot for this long. */
|
|
115
|
+
export declare const AUTOPILOT_COOLDOWN_DAYS = 30;
|
|
116
|
+
/** GSC lag: the after-window must be fully synced before it is judged. */
|
|
117
|
+
export declare const AUTOPILOT_OUTCOME_LAG_DAYS = 3;
|
|
118
|
+
/** Shortest meta description the validator gate accepts (META_DESC_MAX is the ceiling). */
|
|
119
|
+
export declare const AUTOPILOT_DESC_MIN = 70;
|
|
120
|
+
export interface LegacyAutoApplyLever {
|
|
121
|
+
redirectRecoveryAutoApply?: boolean;
|
|
122
|
+
redirectRecoveryAutoApplyMinConfidence?: number;
|
|
123
|
+
/**
|
|
124
|
+
* `updatedBy` of the module-settings document — the advisory owner of a
|
|
125
|
+
* migrated lever. Crons (search sync, digest) rewrite it with
|
|
126
|
+
* `LEGACY_SYSTEM_WRITER`, which is never a user, so that value resolves to
|
|
127
|
+
* "no owner" and the run's preflight picks an active admin instead.
|
|
128
|
+
*/
|
|
129
|
+
updatedBy?: string;
|
|
130
|
+
}
|
|
131
|
+
/** The `updatedBy` value cron writers stamp on the settings document. */
|
|
132
|
+
export declare const LEGACY_SYSTEM_WRITER = "system";
|
|
133
|
+
/**
|
|
134
|
+
* Deep-merge stored policy over defaults, folding the legacy 404 auto-apply
|
|
135
|
+
* lever in per key (spec §1 "Folding"): `types.redirect` and
|
|
136
|
+
* `redirectMinConfidence` fall back to the legacy values only when the stored
|
|
137
|
+
* block does not carry those keys, and a legacy lever that is on also resolves
|
|
138
|
+
* `enabled` / `enabledById` when stored omits them. Explicit stored values
|
|
139
|
+
* always win, so behaviour is continuous for tenants that never touched the
|
|
140
|
+
* new block while remaining fully overridable.
|
|
141
|
+
*
|
|
142
|
+
* A legacy-derived owner is advisory (see `LegacyAutoApplyLever.updatedBy`);
|
|
143
|
+
* the run preflight migrates it to a verified active admin when it cannot be
|
|
144
|
+
* used, rather than pausing the tenant.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveAutopilotPolicy(stored: Partial<SeoAutopilotPolicy> | undefined | null, legacy: LegacyAutoApplyLever): SeoAutopilotPolicy;
|
|
147
|
+
/**
|
|
148
|
+
* A validated PUT body. `outcomeWatch` is itself partial — only the keys the
|
|
149
|
+
* client sent — so the route must deep-merge it over the current policy's
|
|
150
|
+
* `outcomeWatch` rather than replacing the block.
|
|
151
|
+
*/
|
|
152
|
+
export type AutopilotPolicyPatch = Omit<Partial<SeoAutopilotPolicy>, 'outcomeWatch'> & {
|
|
153
|
+
outcomeWatch?: Partial<AutopilotOutcomeWatch>;
|
|
154
|
+
};
|
|
155
|
+
export type PolicyPatchResult = {
|
|
156
|
+
ok: true;
|
|
157
|
+
patch: AutopilotPolicyPatch;
|
|
158
|
+
resume: boolean;
|
|
159
|
+
resumeTypes: AutopilotIssueType[];
|
|
160
|
+
} | {
|
|
161
|
+
ok: false;
|
|
162
|
+
error: string;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Validate a `PUT /seo/autopilot/settings` body. Owner and pause state are
|
|
166
|
+
* never taken from the body — the route derives them (spec §1 Settings API).
|
|
167
|
+
*/
|
|
168
|
+
export declare function parseAutopilotPolicyPatch(body: unknown): PolicyPatchResult;
|
|
169
|
+
//# sourceMappingURL=autopilot-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autopilot-policy.d.ts","sourceRoot":"","sources":["../../src/seo/autopilot-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO Autopilot policy — the per-issue-type opt-in, thresholds, caps, and
|
|
3
|
+
* pause state that govern unattended apply. Stored on
|
|
4
|
+
* `SeoModuleSettings.autopilot` (spec §1). The AI layer's governance flags
|
|
5
|
+
* (`approvalRequired` / `allowBulkApply`) remain a separate veto.
|
|
6
|
+
*/
|
|
7
|
+
export const AUTOPILOT_METADATA_TYPES = [
|
|
8
|
+
'missing-meta-title',
|
|
9
|
+
'meta-title-length',
|
|
10
|
+
'duplicate-meta-title',
|
|
11
|
+
'missing-meta-description',
|
|
12
|
+
'meta-description-length',
|
|
13
|
+
'duplicate-meta-description',
|
|
14
|
+
'missing-canonical',
|
|
15
|
+
'accidental-noindex',
|
|
16
|
+
'missing-structured-data',
|
|
17
|
+
];
|
|
18
|
+
export const AUTOPILOT_ISSUE_TYPES = [...AUTOPILOT_METADATA_TYPES, 'redirect'];
|
|
19
|
+
const TYPE_SET = new Set(AUTOPILOT_ISSUE_TYPES);
|
|
20
|
+
export function isAutopilotIssueType(value) {
|
|
21
|
+
return typeof value === 'string' && TYPE_SET.has(value);
|
|
22
|
+
}
|
|
23
|
+
export const DEFAULT_AUTOPILOT_POLICY = {
|
|
24
|
+
enabled: false,
|
|
25
|
+
enabledById: null,
|
|
26
|
+
pausedAt: null,
|
|
27
|
+
pausedReason: null,
|
|
28
|
+
types: {},
|
|
29
|
+
pausedTypes: {},
|
|
30
|
+
minBrandAlignment: 85,
|
|
31
|
+
redirectMinConfidence: 85,
|
|
32
|
+
maxPerRun: 5,
|
|
33
|
+
maxPerDay: 20,
|
|
34
|
+
verifyFailuresPerRun: 2,
|
|
35
|
+
outcomeWatch: { enabled: true, days: 14, dropRatio: 0.4, minPriorClicks: 50 },
|
|
36
|
+
};
|
|
37
|
+
export const AUTOPILOT_LIMITS = {
|
|
38
|
+
maxPerRun: { min: 1, max: 25 },
|
|
39
|
+
maxPerDay: { min: 1, max: 100 },
|
|
40
|
+
verifyFailuresPerRun: { min: 1, max: 10 },
|
|
41
|
+
minBrandAlignment: { min: 0, max: 100 },
|
|
42
|
+
redirectMinConfidence: { min: 50, max: 100 },
|
|
43
|
+
outcomeDays: { min: 7, max: 28 },
|
|
44
|
+
dropRatio: { min: 0.2, max: 0.8 },
|
|
45
|
+
minPriorClicks: { min: 0, max: 100_000 },
|
|
46
|
+
};
|
|
47
|
+
/** A reverted change keeps its issue off autopilot for this long. */
|
|
48
|
+
export const AUTOPILOT_COOLDOWN_DAYS = 30;
|
|
49
|
+
/** GSC lag: the after-window must be fully synced before it is judged. */
|
|
50
|
+
export const AUTOPILOT_OUTCOME_LAG_DAYS = 3;
|
|
51
|
+
/** Shortest meta description the validator gate accepts (META_DESC_MAX is the ceiling). */
|
|
52
|
+
export const AUTOPILOT_DESC_MIN = 70;
|
|
53
|
+
function isRecord(v) {
|
|
54
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
55
|
+
}
|
|
56
|
+
function pickTypes(raw) {
|
|
57
|
+
const out = {};
|
|
58
|
+
if (!isRecord(raw))
|
|
59
|
+
return out;
|
|
60
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
61
|
+
if (isAutopilotIssueType(k) && typeof v === 'boolean')
|
|
62
|
+
out[k] = v;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function pickPausedTypes(raw) {
|
|
67
|
+
const out = {};
|
|
68
|
+
if (!isRecord(raw))
|
|
69
|
+
return out;
|
|
70
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
71
|
+
if (isAutopilotIssueType(k) && typeof v === 'string')
|
|
72
|
+
out[k] = v;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function num(v, fallback) {
|
|
77
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
78
|
+
}
|
|
79
|
+
/** The `updatedBy` value cron writers stamp on the settings document. */
|
|
80
|
+
export const LEGACY_SYSTEM_WRITER = 'system';
|
|
81
|
+
function legacyOwnerFrom(updatedBy) {
|
|
82
|
+
if (typeof updatedBy !== 'string')
|
|
83
|
+
return null;
|
|
84
|
+
const id = updatedBy.trim();
|
|
85
|
+
return id.length > 0 && id !== LEGACY_SYSTEM_WRITER ? id : null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Deep-merge stored policy over defaults, folding the legacy 404 auto-apply
|
|
89
|
+
* lever in per key (spec §1 "Folding"): `types.redirect` and
|
|
90
|
+
* `redirectMinConfidence` fall back to the legacy values only when the stored
|
|
91
|
+
* block does not carry those keys, and a legacy lever that is on also resolves
|
|
92
|
+
* `enabled` / `enabledById` when stored omits them. Explicit stored values
|
|
93
|
+
* always win, so behaviour is continuous for tenants that never touched the
|
|
94
|
+
* new block while remaining fully overridable.
|
|
95
|
+
*
|
|
96
|
+
* A legacy-derived owner is advisory (see `LegacyAutoApplyLever.updatedBy`);
|
|
97
|
+
* the run preflight migrates it to a verified active admin when it cannot be
|
|
98
|
+
* used, rather than pausing the tenant.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveAutopilotPolicy(stored, legacy) {
|
|
101
|
+
const d = DEFAULT_AUTOPILOT_POLICY;
|
|
102
|
+
const legacyOn = legacy.redirectRecoveryAutoApply === true;
|
|
103
|
+
const s = stored ? stored : {};
|
|
104
|
+
const pausedReason = s.pausedReason;
|
|
105
|
+
const types = pickTypes(s.types);
|
|
106
|
+
if (!('redirect' in types) && legacyOn)
|
|
107
|
+
types.redirect = true;
|
|
108
|
+
const redirectMinConfidence = num(s.redirectMinConfidence, legacyOn
|
|
109
|
+
? num(legacy.redirectRecoveryAutoApplyMinConfidence, d.redirectMinConfidence)
|
|
110
|
+
: d.redirectMinConfidence);
|
|
111
|
+
return {
|
|
112
|
+
enabled: typeof s.enabled === 'boolean' ? s.enabled : legacyOn ? true : d.enabled,
|
|
113
|
+
enabledById: typeof s.enabledById === 'string'
|
|
114
|
+
? s.enabledById
|
|
115
|
+
: legacyOn
|
|
116
|
+
? legacyOwnerFrom(legacy.updatedBy)
|
|
117
|
+
: null,
|
|
118
|
+
pausedAt: typeof s.pausedAt === 'string' ? s.pausedAt : null,
|
|
119
|
+
pausedReason: pausedReason === 'outcome_regression' ||
|
|
120
|
+
pausedReason === 'owner_unavailable' ||
|
|
121
|
+
pausedReason === 'manual'
|
|
122
|
+
? pausedReason
|
|
123
|
+
: null,
|
|
124
|
+
types,
|
|
125
|
+
pausedTypes: pickPausedTypes(s.pausedTypes),
|
|
126
|
+
minBrandAlignment: num(s.minBrandAlignment, d.minBrandAlignment),
|
|
127
|
+
redirectMinConfidence,
|
|
128
|
+
maxPerRun: num(s.maxPerRun, d.maxPerRun),
|
|
129
|
+
maxPerDay: num(s.maxPerDay, d.maxPerDay),
|
|
130
|
+
verifyFailuresPerRun: num(s.verifyFailuresPerRun, d.verifyFailuresPerRun),
|
|
131
|
+
outcomeWatch: {
|
|
132
|
+
enabled: isRecord(s.outcomeWatch) && typeof s.outcomeWatch.enabled === 'boolean'
|
|
133
|
+
? s.outcomeWatch.enabled
|
|
134
|
+
: d.outcomeWatch.enabled,
|
|
135
|
+
days: num(isRecord(s.outcomeWatch) ? s.outcomeWatch.days : undefined, d.outcomeWatch.days),
|
|
136
|
+
dropRatio: num(isRecord(s.outcomeWatch) ? s.outcomeWatch.dropRatio : undefined, d.outcomeWatch.dropRatio),
|
|
137
|
+
minPriorClicks: num(isRecord(s.outcomeWatch) ? s.outcomeWatch.minPriorClicks : undefined, d.outcomeWatch.minPriorClicks),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function intIn(v, lo, hi) {
|
|
142
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= lo && v <= hi;
|
|
143
|
+
}
|
|
144
|
+
function numIn(v, lo, hi) {
|
|
145
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= lo && v <= hi;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validate a `PUT /seo/autopilot/settings` body. Owner and pause state are
|
|
149
|
+
* never taken from the body — the route derives them (spec §1 Settings API).
|
|
150
|
+
*/
|
|
151
|
+
export function parseAutopilotPolicyPatch(body) {
|
|
152
|
+
if (!isRecord(body))
|
|
153
|
+
return { ok: false, error: 'Request body must be an object' };
|
|
154
|
+
const L = AUTOPILOT_LIMITS;
|
|
155
|
+
const patch = {};
|
|
156
|
+
if ('enabled' in body) {
|
|
157
|
+
if (typeof body.enabled !== 'boolean')
|
|
158
|
+
return { ok: false, error: 'enabled must be a boolean' };
|
|
159
|
+
patch.enabled = body.enabled;
|
|
160
|
+
}
|
|
161
|
+
if ('types' in body) {
|
|
162
|
+
if (!isRecord(body.types))
|
|
163
|
+
return { ok: false, error: 'types must be an object' };
|
|
164
|
+
const types = {};
|
|
165
|
+
for (const [k, v] of Object.entries(body.types)) {
|
|
166
|
+
if (!isAutopilotIssueType(k)) {
|
|
167
|
+
return { ok: false, error: `types contains an unknown issue type: ${k}` };
|
|
168
|
+
}
|
|
169
|
+
if (typeof v !== 'boolean')
|
|
170
|
+
return { ok: false, error: `types.${k} must be a boolean` };
|
|
171
|
+
types[k] = v;
|
|
172
|
+
}
|
|
173
|
+
patch.types = types;
|
|
174
|
+
}
|
|
175
|
+
const intFields = [
|
|
176
|
+
['maxPerRun', L.maxPerRun],
|
|
177
|
+
['maxPerDay', L.maxPerDay],
|
|
178
|
+
['verifyFailuresPerRun', L.verifyFailuresPerRun],
|
|
179
|
+
];
|
|
180
|
+
for (const [key, range] of intFields) {
|
|
181
|
+
if (key in body) {
|
|
182
|
+
if (!intIn(body[key], range.min, range.max)) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: `${key} must be an integer between ${range.min} and ${range.max}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
;
|
|
189
|
+
patch[key] = body[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const numFields = [
|
|
193
|
+
['minBrandAlignment', L.minBrandAlignment],
|
|
194
|
+
['redirectMinConfidence', L.redirectMinConfidence],
|
|
195
|
+
];
|
|
196
|
+
for (const [key, range] of numFields) {
|
|
197
|
+
if (key in body) {
|
|
198
|
+
if (!numIn(body[key], range.min, range.max)) {
|
|
199
|
+
return { ok: false, error: `${key} must be a number between ${range.min} and ${range.max}` };
|
|
200
|
+
}
|
|
201
|
+
;
|
|
202
|
+
patch[key] = body[key];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if ('outcomeWatch' in body) {
|
|
206
|
+
if (!isRecord(body.outcomeWatch))
|
|
207
|
+
return { ok: false, error: 'outcomeWatch must be an object' };
|
|
208
|
+
const ow = body.outcomeWatch;
|
|
209
|
+
const out = {};
|
|
210
|
+
if ('enabled' in ow) {
|
|
211
|
+
if (typeof ow.enabled !== 'boolean') {
|
|
212
|
+
return { ok: false, error: 'outcomeWatch.enabled must be a boolean' };
|
|
213
|
+
}
|
|
214
|
+
out.enabled = ow.enabled;
|
|
215
|
+
}
|
|
216
|
+
if ('days' in ow) {
|
|
217
|
+
if (!intIn(ow.days, L.outcomeDays.min, L.outcomeDays.max)) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
error: `outcomeWatch.days must be an integer between ${L.outcomeDays.min} and ${L.outcomeDays.max}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
out.days = ow.days;
|
|
224
|
+
}
|
|
225
|
+
if ('dropRatio' in ow) {
|
|
226
|
+
if (!numIn(ow.dropRatio, L.dropRatio.min, L.dropRatio.max)) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: `outcomeWatch.dropRatio must be a number between ${L.dropRatio.min} and ${L.dropRatio.max}`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
out.dropRatio = ow.dropRatio;
|
|
233
|
+
}
|
|
234
|
+
if ('minPriorClicks' in ow) {
|
|
235
|
+
if (!intIn(ow.minPriorClicks, L.minPriorClicks.min, L.minPriorClicks.max)) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
error: `outcomeWatch.minPriorClicks must be an integer between ${L.minPriorClicks.min} and ${L.minPriorClicks.max}`,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
out.minPriorClicks = ow.minPriorClicks;
|
|
242
|
+
}
|
|
243
|
+
// Partial on purpose: only the keys sent are present. The route must
|
|
244
|
+
// deep-merge this over the current policy's outcomeWatch, not replace it.
|
|
245
|
+
patch.outcomeWatch = out;
|
|
246
|
+
}
|
|
247
|
+
let resume = false;
|
|
248
|
+
if ('resume' in body) {
|
|
249
|
+
if (typeof body.resume !== 'boolean')
|
|
250
|
+
return { ok: false, error: 'resume must be a boolean' };
|
|
251
|
+
resume = body.resume;
|
|
252
|
+
}
|
|
253
|
+
const resumeTypes = [];
|
|
254
|
+
if ('resumeTypes' in body) {
|
|
255
|
+
if (!Array.isArray(body.resumeTypes)) {
|
|
256
|
+
return { ok: false, error: 'resumeTypes must be an array' };
|
|
257
|
+
}
|
|
258
|
+
for (const t of body.resumeTypes) {
|
|
259
|
+
if (!isAutopilotIssueType(t)) {
|
|
260
|
+
return { ok: false, error: `resumeTypes contains an unknown issue type: ${String(t)}` };
|
|
261
|
+
}
|
|
262
|
+
resumeTypes.push(t);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, patch, resume, resumeTypes };
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=autopilot-policy.js.map
|