@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/ai/governance.d.ts +6 -0
  3. package/dist/ai/governance.d.ts.map +1 -1
  4. package/dist/ai/governance.js +19 -0
  5. package/dist/ai/governance.js.map +1 -1
  6. package/dist/api/handlers.d.ts.map +1 -1
  7. package/dist/api/handlers.js +2 -0
  8. package/dist/api/handlers.js.map +1 -1
  9. package/dist/api/routes/redirects.d.ts.map +1 -1
  10. package/dist/api/routes/redirects.js +89 -93
  11. package/dist/api/routes/redirects.js.map +1 -1
  12. package/dist/api/routes/seo-autopilot.d.ts +12 -0
  13. package/dist/api/routes/seo-autopilot.d.ts.map +1 -0
  14. package/dist/api/routes/seo-autopilot.js +257 -0
  15. package/dist/api/routes/seo-autopilot.js.map +1 -0
  16. package/dist/api/routes/seo.d.ts.map +1 -1
  17. package/dist/api/routes/seo.js +195 -360
  18. package/dist/api/routes/seo.js.map +1 -1
  19. package/dist/db/model-types.d.ts +26 -0
  20. package/dist/db/model-types.d.ts.map +1 -1
  21. package/dist/generated/browser.d.ts +8 -0
  22. package/dist/generated/client.d.ts +8 -0
  23. package/dist/generated/internal/class.d.ts +11 -0
  24. package/dist/generated/internal/class.js +4 -4
  25. package/dist/generated/internal/prismaNamespace.d.ts +120 -1
  26. package/dist/generated/internal/prismaNamespace.js +42 -0
  27. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +44 -0
  28. package/dist/generated/internal/prismaNamespaceBrowser.js +42 -0
  29. package/dist/generated/models/SeoAutoApply.d.ts +1495 -0
  30. package/dist/generated/models/SeoAutoApply.js +1 -0
  31. package/dist/generated/models.d.ts +1 -0
  32. package/dist/redirects/recovery.d.ts +15 -9
  33. package/dist/redirects/recovery.d.ts.map +1 -1
  34. package/dist/redirects/recovery.js +15 -61
  35. package/dist/redirects/recovery.js.map +1 -1
  36. package/dist/redirects/suggestion-accept.d.ts +29 -0
  37. package/dist/redirects/suggestion-accept.d.ts.map +1 -0
  38. package/dist/redirects/suggestion-accept.js +108 -0
  39. package/dist/redirects/suggestion-accept.js.map +1 -0
  40. package/dist/seo/audit-runner.d.ts.map +1 -1
  41. package/dist/seo/audit-runner.js +0 -2
  42. package/dist/seo/audit-runner.js.map +1 -1
  43. package/dist/seo/autopilot-gate.d.ts +101 -0
  44. package/dist/seo/autopilot-gate.d.ts.map +1 -0
  45. package/dist/seo/autopilot-gate.js +272 -0
  46. package/dist/seo/autopilot-gate.js.map +1 -0
  47. package/dist/seo/autopilot-ledger.d.ts +74 -0
  48. package/dist/seo/autopilot-ledger.d.ts.map +1 -0
  49. package/dist/seo/autopilot-ledger.js +118 -0
  50. package/dist/seo/autopilot-ledger.js.map +1 -0
  51. package/dist/seo/autopilot-outcome.d.ts +93 -0
  52. package/dist/seo/autopilot-outcome.d.ts.map +1 -0
  53. package/dist/seo/autopilot-outcome.js +206 -0
  54. package/dist/seo/autopilot-outcome.js.map +1 -0
  55. package/dist/seo/autopilot-policy.d.ts +169 -0
  56. package/dist/seo/autopilot-policy.d.ts.map +1 -0
  57. package/dist/seo/autopilot-policy.js +267 -0
  58. package/dist/seo/autopilot-policy.js.map +1 -0
  59. package/dist/seo/autopilot-revert.d.ts +40 -0
  60. package/dist/seo/autopilot-revert.d.ts.map +1 -0
  61. package/dist/seo/autopilot-revert.js +155 -0
  62. package/dist/seo/autopilot-revert.js.map +1 -0
  63. package/dist/seo/autopilot.d.ts +91 -0
  64. package/dist/seo/autopilot.d.ts.map +1 -0
  65. package/dist/seo/autopilot.js +1060 -0
  66. package/dist/seo/autopilot.js.map +1 -0
  67. package/dist/seo/config-store.d.ts +35 -2
  68. package/dist/seo/config-store.d.ts.map +1 -1
  69. package/dist/seo/config-store.js +58 -1
  70. package/dist/seo/config-store.js.map +1 -1
  71. package/dist/seo/deep-equal.d.ts +12 -0
  72. package/dist/seo/deep-equal.d.ts.map +1 -0
  73. package/dist/seo/deep-equal.js +42 -0
  74. package/dist/seo/deep-equal.js.map +1 -0
  75. package/dist/seo/digest.d.ts +27 -0
  76. package/dist/seo/digest.d.ts.map +1 -1
  77. package/dist/seo/digest.js +77 -1
  78. package/dist/seo/digest.js.map +1 -1
  79. package/dist/seo/index.d.ts +20 -2
  80. package/dist/seo/index.d.ts.map +1 -1
  81. package/dist/seo/index.js +12 -1
  82. package/dist/seo/index.js.map +1 -1
  83. package/dist/seo/issue-fix-ai.d.ts +40 -1
  84. package/dist/seo/issue-fix-ai.d.ts.map +1 -1
  85. package/dist/seo/issue-fix-ai.js +53 -1
  86. package/dist/seo/issue-fix-ai.js.map +1 -1
  87. package/dist/seo/issue-fix-apply.d.ts +104 -0
  88. package/dist/seo/issue-fix-apply.d.ts.map +1 -0
  89. package/dist/seo/issue-fix-apply.js +406 -0
  90. package/dist/seo/issue-fix-apply.js.map +1 -0
  91. package/dist/seo/issue-fix.d.ts +9 -0
  92. package/dist/seo/issue-fix.d.ts.map +1 -1
  93. package/dist/seo/issue-fix.js +1 -0
  94. package/dist/seo/issue-fix.js.map +1 -1
  95. package/dist/seo/proposals-bulk-apply.d.ts +52 -0
  96. package/dist/seo/proposals-bulk-apply.d.ts.map +1 -0
  97. package/dist/seo/proposals-bulk-apply.js +183 -0
  98. package/dist/seo/proposals-bulk-apply.js.map +1 -0
  99. package/dist/seo/proposals-generate.d.ts +45 -0
  100. package/dist/seo/proposals-generate.d.ts.map +1 -0
  101. package/dist/seo/proposals-generate.js +144 -0
  102. package/dist/seo/proposals-generate.js.map +1 -0
  103. package/dist/seo/proposals.d.ts +125 -0
  104. package/dist/seo/proposals.d.ts.map +1 -0
  105. package/dist/seo/proposals.js +270 -0
  106. package/dist/seo/proposals.js.map +1 -0
  107. package/package.json +1 -1
  108. package/prisma/migrations/0017_seo_auto_applies/migration.sql +35 -0
  109. 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":"AAgBA,OAAO,EACL,0BAA0B,EAI1B,oBAAoB,GACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,aAAa,EAA4B,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAE1D,uDAAuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAA;AACtC,oFAAoF;AACpF,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAA;AAC5C,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAClC,iFAAiF;AACjF,MAAM,aAAa,GAAG,IAAI,CAAA;AAO1B;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EACb,KAAa,EACb,KAA4B;IAE5B,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,cAAc;QAAE,OAAO,mBAAmB,CAAA;IAC1E,IAAI,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,aAAa;QAAE,OAAO,WAAW,CAAA;IAC9E,OAAO,MAAM,CAAA;AACf,CAAC;AA2CD,KAAK,UAAU,SAAS,CAAC,EAAY,EAAE,IAAY,EAAE,IAAU,EAAE,EAAQ;IACvE,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC;QAC7C,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE;QAC7C,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;KACvB,CAAC,CAAA;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,MAAM,CAAC,CAAO;IACrB,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACrC,CAAC;AAED,uEAAuE;AACvE,SAAS,mBAAmB,CAAC,OAAgB;IAC3C,IAAI,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,MAAM,CAAC,GAAI,OAAmC,CAAC,cAAc,CAAA;QAC7D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,EAAY,EACZ,MAA0B,EAC1B,IAAyB;IAEzB,MAAM,MAAM,GAAuB;QACjC,SAAS,EAAE,CAAC;QACZ,IAAI,EAAE,CAAC;QACP,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,CAAC;QACX,YAAY,EAAE,CAAC;QACf,SAAS,EAAE,CAAC;QACZ,YAAY,EAAE,CAAC;QACf,QAAQ,EAAE,CAAC;QACX,WAAW,EAAE,KAAK;QAClB,WAAW,EAAE,EAAE;KAChB,CAAA;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAA;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,MAAM,CAAA;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,CAAA;IACpC,MAAM,YAAY,GAAG,GAAY,EAAE,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAA;IAE3F,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC;QAChD,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACzB,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;KACvB,CAAC,CAAA;IACF,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,CAAA;IAE1B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAA;IAChD,MAAM,SAAS,GAAG,IAAI,IAAI,CACxB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,GAAG,0BAA0B,CAAC,GAAG,MAAM,CACxE,CAAA;IAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC;QAC1C,KAAK,EAAE;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,SAAS;YAClB,SAAS,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE;SAC9B;QACD,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE;QAC7B,IAAI,EAAE,oBAAoB;KAC3B,CAAC,CAAA;IAEF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,YAAY,EAAE,EAAE,CAAC;YACnB,MAAM,CAAC,WAAW,GAAG,IAAI,CAAA;YACzB,MAAK;QACP,CAAC;QACD,2EAA2E;QAC3E,IAAI,CAAC,GAAG,CAAC,IAAI;YAAE,SAAQ;QACvB,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACxC,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,CAAA;QAC/D,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,CAAA;QAChD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,CAAA;QAClD,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC,CAAA;QAC7D,IAAI,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;YAC1B,MAAM,CAAC,QAAQ,EAAE,CAAA;YACjB,SAAQ;QACV,CAAC;QACD,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACvC,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC;YAC3C,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC;SAC5C,CAAC,CAAA;QACF,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;QACtD,MAAM,CAAC,SAAS,EAAE,CAAA;QAClB,MAAM,SAAS,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,CAAA;QAC3C,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;QAChE,MAAM,OAAO,GAAG;YACd,WAAW,EAAE,KAAK;YAClB,WAAW,EAAE,KAAK;YAClB,SAAS;YACT,UAAU,EAAE,KAAK,CAAC,IAAI;YACtB,YAAY,EAAE;gBACZ,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC;gBAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;gBACxB,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC;gBAC5B,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;aACzB;YACD,GAAG,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtE,CAAA;QACD,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;YAC3B,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE;YACrB,IAAI,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,EAAE;SAClF,CAAC,CAAA;QACF,QAAQ,SAAS,EAAE,CAAC;YAClB,KAAK,mBAAmB;gBACtB,MAAM,CAAC,YAAY,EAAE,CAAA;gBACrB,MAAK;YACP,KAAK,MAAM;gBACT,MAAM,CAAC,IAAI,EAAE,CAAA;gBACb,MAAK;YACP,KAAK,WAAW,CAAC,CAAC,CAAC;gBACjB,MAAM,CAAC,SAAS,EAAE,CAAA;gBAClB,2EAA2E;gBAC3E,sEAAsE;gBACtE,IAAI,oBAAoB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBACxC,MAAM,MAAM,GAAG,UAAU,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,SAAS,OAAO,oCAAoC,CAAA;oBAC1G,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,MAAM,CAAA;oBAC1C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;wBACjB,IAAI,CAAC;4BACH,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;wBAC3C,CAAC;wBAAC,OAAO,QAAQ,EAAE,CAAC;4BAClB,OAAO,CAAC,IAAI,CAAC,+CAA+C,EAAE;gCAC5D,IAAI,EAAE,GAAG,CAAC,SAAS;gCACnB,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC;6BACxB,CAAC,CAAA;wBACJ,CAAC;oBACH,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE;wBACvC,KAAK,EAAE,IAAI,CAAC,KAAK;wBACjB,MAAM,EAAE,oBAAoB;wBAC5B,KAAK,EAAE,KAAK;qBACb,CAAC,CAAA;oBACF,IAAI,OAAO,CAAC,MAAM,KAAK,UAAU;wBAAE,MAAM,CAAC,QAAQ,EAAE,CAAA;yBAC/C,IAAI,OAAO,CAAC,MAAM,KAAK,UAAU;wBAAE,MAAM,CAAC,SAAS,EAAE,CAAA;gBAC5D,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,YAAY,EAAE,CAAA;oBACrB,OAAO,CAAC,IAAI,CAAC,uCAAuC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;oBACzF,MAAM,QAAQ,GAAG,gBAAgB,GAAG,CAAC,CAAA;oBACrC,yEAAyE;oBACzE,MAAM,OAAO,GAAG,QAAQ,GAAG,2BAA2B,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAA;oBAChF,IAAI,CAAC;wBACH,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;4BAC3B,KAAK,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE;4BACrB,IAAI,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE;yBAC5E,CAAC,CAAA;oBACJ,CAAC;oBAAC,OAAO,UAAU,EAAE,CAAC;wBACpB,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE;4BACrD,EAAE,EAAE,GAAG,CAAC,EAAE;4BACV,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC;yBAC1B,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC;gBACD,MAAK;YACP,CAAC;YACD,OAAO,CAAC,CAAC,CAAC;gBACR,MAAM,UAAU,GAAU,SAAS,CAAA;gBACnC,OAAO,UAAU,CAAA;YACnB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -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