@prenta/core 5.5.0 → 5.6.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 (44) hide show
  1. package/CHANGELOG.md +21 -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/routes/redirects.d.ts.map +1 -1
  7. package/dist/api/routes/redirects.js +17 -65
  8. package/dist/api/routes/redirects.js.map +1 -1
  9. package/dist/api/routes/seo.d.ts.map +1 -1
  10. package/dist/api/routes/seo.js +195 -360
  11. package/dist/api/routes/seo.js.map +1 -1
  12. package/dist/redirects/suggestion-accept.d.ts +28 -0
  13. package/dist/redirects/suggestion-accept.d.ts.map +1 -0
  14. package/dist/redirects/suggestion-accept.js +98 -0
  15. package/dist/redirects/suggestion-accept.js.map +1 -0
  16. package/dist/seo/index.d.ts +6 -2
  17. package/dist/seo/index.d.ts.map +1 -1
  18. package/dist/seo/index.js +5 -1
  19. package/dist/seo/index.js.map +1 -1
  20. package/dist/seo/issue-fix-ai.d.ts +40 -1
  21. package/dist/seo/issue-fix-ai.d.ts.map +1 -1
  22. package/dist/seo/issue-fix-ai.js +53 -1
  23. package/dist/seo/issue-fix-ai.js.map +1 -1
  24. package/dist/seo/issue-fix-apply.d.ts +85 -0
  25. package/dist/seo/issue-fix-apply.d.ts.map +1 -0
  26. package/dist/seo/issue-fix-apply.js +400 -0
  27. package/dist/seo/issue-fix-apply.js.map +1 -0
  28. package/dist/seo/issue-fix.d.ts +9 -0
  29. package/dist/seo/issue-fix.d.ts.map +1 -1
  30. package/dist/seo/issue-fix.js +1 -0
  31. package/dist/seo/issue-fix.js.map +1 -1
  32. package/dist/seo/proposals-bulk-apply.d.ts +52 -0
  33. package/dist/seo/proposals-bulk-apply.d.ts.map +1 -0
  34. package/dist/seo/proposals-bulk-apply.js +183 -0
  35. package/dist/seo/proposals-bulk-apply.js.map +1 -0
  36. package/dist/seo/proposals-generate.d.ts +43 -0
  37. package/dist/seo/proposals-generate.d.ts.map +1 -0
  38. package/dist/seo/proposals-generate.js +138 -0
  39. package/dist/seo/proposals-generate.js.map +1 -0
  40. package/dist/seo/proposals.d.ts +116 -0
  41. package/dist/seo/proposals.d.ts.map +1 -0
  42. package/dist/seo/proposals.js +236 -0
  43. package/dist/seo/proposals.js.map +1 -0
  44. package/package.json +1 -1
@@ -0,0 +1,183 @@
1
+ import { assertBulkApplyAllowed as defaultAssertBulk } from '../ai/governance.js';
2
+ import { acceptRedirectSuggestion as defaultAcceptRedirect } from '../redirects/suggestion-accept.js';
3
+ import { applyIssueFix as defaultApplyIssue } from './issue-fix-apply.js';
4
+ import { pendingFixFromMetadata } from './issue-fix-ai.js';
5
+ import { PROPOSAL_BATCH_TIME_BUDGET_MS } from './proposals-generate.js';
6
+ import { ISSUE_FIX_FEATURE_KEY, REDIRECT_FEATURE_KEY } from './proposals.js';
7
+ export const PROPOSAL_BULK_APPLY_MAX = 50;
8
+ const BULK_APPLY_CONCURRENCY = 3;
9
+ const ADMIN_ROLES = new Set(['OWNER', 'ADMIN']);
10
+ const REDIRECT_MODEL_MISSING_MESSAGE = 'The "RedirectSuggestion" model is not available in your Prisma schema.';
11
+ function toDate(value) {
12
+ return value instanceof Date ? value : new Date(String(value));
13
+ }
14
+ const keyOf = (item) => `${item.kind}:${item.id}`;
15
+ /**
16
+ * Same probe as `hasModel()` in `api/route-helpers.ts` (and the local copy in
17
+ * `redirects/suggestion-accept.ts`): `guardedDb()` hands back a throwing Proxy
18
+ * for models missing from the consumer schema, so a plain truthiness check
19
+ * would report them present. The accept ROUTE gates this; a direct service
20
+ * caller must gate it itself, and the seo layer must not depend on the API layer.
21
+ */
22
+ function hasModel(db, model) {
23
+ try {
24
+ const delegate = db[model];
25
+ return Boolean(delegate) && typeof delegate?.findMany === 'function';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ export async function bulkApplySeoProposals(db, input, deps = {}) {
32
+ const applyIssue = deps.applyIssue ?? defaultApplyIssue;
33
+ const acceptRedirect = deps.acceptRedirect ?? defaultAcceptRedirect;
34
+ const assertBulk = deps.assertBulk ?? defaultAssertBulk;
35
+ const now = deps.now ?? Date.now;
36
+ const budgetMs = deps.budgetMs ?? PROPOSAL_BATCH_TIME_BUDGET_MS;
37
+ const concurrency = Math.max(1, deps.concurrency ?? BULK_APPLY_CONCURRENCY);
38
+ const { actor } = input;
39
+ const startedAt = now();
40
+ // Dedupe by `kind:id`, keeping first-occurrence order. Results are keyed the
41
+ // same way, so a repeated item would run twice in one group — the second run
42
+ // finds the cache consumed and its `not-fixable`/`not-found` would overwrite
43
+ // the first run's real `applied`, and governance would count it as extra load.
44
+ const seen = new Set();
45
+ const items = input.items.filter((item) => {
46
+ const key = keyOf(item);
47
+ if (seen.has(key))
48
+ return false;
49
+ seen.add(key);
50
+ return true;
51
+ });
52
+ // A single item is the ordinary approve path; governance only weighs in on
53
+ // batches. Keys are always derived from the kinds actually present —
54
+ // `assertBulkApplyAllowed` passes an empty key list, so never send one.
55
+ if (items.length > 1) {
56
+ const keys = [];
57
+ if (items.some((i) => i.kind === 'issue-fix'))
58
+ keys.push(ISSUE_FIX_FEATURE_KEY);
59
+ if (items.some((i) => i.kind === 'redirect'))
60
+ keys.push(REDIRECT_FEATURE_KEY);
61
+ const decision = await assertBulk(db, keys, items.length);
62
+ if (!decision.ok)
63
+ return { blocked: true, reason: decision.reason };
64
+ }
65
+ const redirectModelAvailable = items.some((i) => i.kind === 'redirect')
66
+ ? hasModel(db, 'redirectSuggestion')
67
+ : true;
68
+ // Resolve which document each issue-fix item writes (insert-link fixes
69
+ // write the SOURCE document) and snapshot those documents' updatedAt.
70
+ const issueIds = items.filter((i) => i.kind === 'issue-fix').map((i) => i.id);
71
+ const writeTargetByIssue = new Map();
72
+ if (issueIds.length > 0) {
73
+ const rows = await db.seoIssue.findMany({ where: { id: { in: issueIds } } });
74
+ for (const row of rows) {
75
+ const cache = pendingFixFromMetadata(row.metadata);
76
+ const source = cache && typeof cache.patch.sourceEntityId === 'string' ? cache.patch.sourceEntityId : null;
77
+ const target = source ?? row.entityId ?? null;
78
+ if (target)
79
+ writeTargetByIssue.set(row.id, target);
80
+ }
81
+ }
82
+ const snapshotByDoc = new Map();
83
+ const docIds = [...new Set(writeTargetByIssue.values())];
84
+ if (docIds.length > 0) {
85
+ const docs = (await db.document.findMany({
86
+ where: { id: { in: docIds }, deletedAt: null },
87
+ select: { id: true, updatedAt: true },
88
+ }));
89
+ for (const doc of docs)
90
+ snapshotByDoc.set(doc.id, toDate(doc.updatedAt));
91
+ }
92
+ // Group: issue fixes by write-target document; every redirect is its own group.
93
+ const groups = new Map();
94
+ for (const item of items) {
95
+ const key = item.kind === 'redirect'
96
+ ? `redirect:${item.id}`
97
+ : `doc:${writeTargetByIssue.get(item.id) ?? `issue:${item.id}`}`;
98
+ const list = groups.get(key);
99
+ if (list)
100
+ list.push(item);
101
+ else
102
+ groups.set(key, [item]);
103
+ }
104
+ const resultByKey = new Map();
105
+ for (const item of items) {
106
+ resultByKey.set(keyOf(item), { kind: item.kind, id: item.id, status: 'pending' });
107
+ }
108
+ async function runItem(item) {
109
+ try {
110
+ if (item.kind === 'redirect') {
111
+ if (!ADMIN_ROLES.has(actor.role)) {
112
+ return {
113
+ kind: item.kind,
114
+ id: item.id,
115
+ status: 'not-allowed',
116
+ reason: 'Accepting redirects requires an admin role.',
117
+ };
118
+ }
119
+ if (!redirectModelAvailable) {
120
+ return {
121
+ kind: item.kind,
122
+ id: item.id,
123
+ status: 'error',
124
+ reason: REDIRECT_MODEL_MISSING_MESSAGE,
125
+ };
126
+ }
127
+ const r = await acceptRedirect(db, {
128
+ suggestionId: item.id,
129
+ actor: { userId: actor.userId },
130
+ });
131
+ if (r.status === 'accepted')
132
+ return { kind: item.kind, id: item.id, status: 'applied' };
133
+ if (r.status === 'not-found')
134
+ return { kind: item.kind, id: item.id, status: 'not-found' };
135
+ return { kind: item.kind, id: item.id, status: r.status, reason: r.reason };
136
+ }
137
+ const target = writeTargetByIssue.get(item.id);
138
+ const r = await applyIssue(db, {
139
+ issueId: item.id,
140
+ fingerprint: item.fingerprint,
141
+ actor,
142
+ staleAgainst: target ? snapshotByDoc.get(target) : undefined,
143
+ });
144
+ if (r.status === 'applied')
145
+ return { kind: item.kind, id: item.id, status: 'applied' };
146
+ if (r.status === 'plan-required') {
147
+ return { kind: item.kind, id: item.id, status: 'plan-required', reason: r.denial.message };
148
+ }
149
+ return { kind: item.kind, id: item.id, status: r.status, reason: r.reason };
150
+ }
151
+ catch (err) {
152
+ console.warn('[seo/proposals-bulk-apply] item apply failed', { kind: item.kind, id: item.id }, err);
153
+ return {
154
+ kind: item.kind,
155
+ id: item.id,
156
+ status: 'error',
157
+ reason: err instanceof Error ? err.message : 'Apply failed',
158
+ };
159
+ }
160
+ }
161
+ const groupList = [...groups.values()];
162
+ let cursor = 0;
163
+ async function worker() {
164
+ while (cursor < groupList.length) {
165
+ const group = groupList[cursor++];
166
+ for (const item of group) {
167
+ if (now() - startedAt >= budgetMs)
168
+ return;
169
+ resultByKey.set(keyOf(item), await runItem(item));
170
+ }
171
+ }
172
+ }
173
+ await Promise.all(Array.from({ length: Math.min(concurrency, groupList.length) }, worker));
174
+ const results = items.map((item) => resultByKey.get(keyOf(item)));
175
+ return {
176
+ blocked: false,
177
+ results,
178
+ applied: results.filter((r) => r.status === 'applied').length,
179
+ pending: results.filter((r) => r.status === 'pending').length,
180
+ failed: results.filter((r) => r.status !== 'applied' && r.status !== 'pending').length,
181
+ };
182
+ }
183
+ //# sourceMappingURL=proposals-bulk-apply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposals-bulk-apply.js","sourceRoot":"","sources":["../../src/seo/proposals-bulk-apply.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Batch "Generate proposals": run `suggestIssueFix` for eligible open issues
3
+ * and cache the results so the proposals inbox fills without opening issues
4
+ * one by one. Human-triggered only (route is confirm-gated, plan-gated,
5
+ * rate-limited). Bounded three ways — per-call cap, concurrency, wall clock —
6
+ * because it runs inside a 30 s function budget and spends AI tokens.
7
+ *
8
+ * AI failures are split by scope. Provider-level codes (`HALTING_AI_ERROR_CODES`)
9
+ * mean every further call this pass would fail too, so the batch halts and
10
+ * reports `haltedReason`. A per-issue `error` (bad model output for one
11
+ * document) is counted as failed AND stamped with `generateSkippedFingerprint`,
12
+ * so the same issue cannot pin the severity-sorted queue head on every pass —
13
+ * it is retried once the document changes, or when an editor explicitly runs
14
+ * `suggest-fix` (a new cache clears the marker).
15
+ */
16
+ import type { PrentaDB } from '../db/client-types.js';
17
+ import { loadIssueFixContext as defaultLoadIssueFixContext } from './issue-fix-apply.js';
18
+ import { suggestIssueFix as defaultSuggestIssueFix } from './issue-fix.js';
19
+ export declare const PROPOSAL_GENERATE_BATCH_MAX = 10;
20
+ export declare const PROPOSAL_GENERATE_CONCURRENCY = 3;
21
+ export declare const PROPOSAL_BATCH_TIME_BUDGET_MS = 20000;
22
+ export interface GenerateProposalsResult {
23
+ generated: number;
24
+ skipped: number;
25
+ failed: number;
26
+ /** Eligible issues not processed this call (cap, budget, or early halt). */
27
+ remaining: number;
28
+ /** Set when a provider-level AI error (see `HALTING_AI_ERROR_CODES`) stopped the batch early. */
29
+ haltedReason: string | null;
30
+ }
31
+ export interface GenerateProposalsDeps {
32
+ /** Injected for tests; defaults call the real suggestIssueFix / loadIssueFixContext. */
33
+ suggest?: typeof defaultSuggestIssueFix;
34
+ loadContext?: typeof defaultLoadIssueFixContext;
35
+ now?: () => number;
36
+ max?: number;
37
+ concurrency?: number;
38
+ budgetMs?: number;
39
+ }
40
+ export declare function generateSeoProposals(db: PrentaDB, actor: {
41
+ userId: string;
42
+ }, deps?: GenerateProposalsDeps): Promise<GenerateProposalsResult>;
43
+ //# sourceMappingURL=proposals-generate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposals-generate.d.ts","sourceRoot":"","sources":["../../src/seo/proposals-generate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErD,OAAO,EAAE,mBAAmB,IAAI,0BAA0B,EAAE,MAAM,sBAAsB,CAAA;AACxF,OAAO,EAAE,eAAe,IAAI,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAa1E,eAAO,MAAM,2BAA2B,KAAK,CAAA;AAC7C,eAAO,MAAM,6BAA6B,IAAI,CAAA;AAC9C,eAAO,MAAM,6BAA6B,QAAS,CAAA;AAcnD,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,sBAAsB,CAAA;IACvC,WAAW,CAAC,EAAE,OAAO,0BAA0B,CAAA;IAC/C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAUD,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,uBAAuB,CAAC,CAqIlC"}
@@ -0,0 +1,138 @@
1
+ import { getAIConfig } from '../ai/config-store.js';
2
+ import { loadIssueFixContext as defaultLoadIssueFixContext } from './issue-fix-apply.js';
3
+ import { suggestIssueFix as defaultSuggestIssueFix } from './issue-fix.js';
4
+ import { pendingFixFromMetadata, pendingFixToMetadata, scoreFixBrandAlignment, stickyFingerprintsFromMetadata, } from './issue-fix-ai.js';
5
+ import { loadOpenFixableIssuesWithDocs, selectEligibleForGeneration, } from './proposals.js';
6
+ export const PROPOSAL_GENERATE_BATCH_MAX = 10;
7
+ export const PROPOSAL_GENERATE_CONCURRENCY = 3;
8
+ export const PROPOSAL_BATCH_TIME_BUDGET_MS = 20_000;
9
+ /**
10
+ * `aiError.code`s that describe the provider / governance state rather than
11
+ * one issue: every further `suggestIssueFix` call in this pass is guaranteed
12
+ * to fail the same way, so the batch stops instead of burning the queue.
13
+ */
14
+ const HALTING_AI_ERROR_CODES = new Set([
15
+ 'rate_limited',
16
+ 'unavailable',
17
+ 'feature_disabled',
18
+ 'budget_exceeded',
19
+ ]);
20
+ function asRecord(value) {
21
+ return value && typeof value === 'object' && !Array.isArray(value)
22
+ ? value
23
+ : {};
24
+ }
25
+ export async function generateSeoProposals(db, actor, deps = {}) {
26
+ const suggest = deps.suggest ?? defaultSuggestIssueFix;
27
+ const loadContext = deps.loadContext ?? defaultLoadIssueFixContext;
28
+ const now = deps.now ?? Date.now;
29
+ const max = deps.max ?? PROPOSAL_GENERATE_BATCH_MAX;
30
+ const concurrency = Math.max(1, deps.concurrency ?? PROPOSAL_GENERATE_CONCURRENCY);
31
+ const budgetMs = deps.budgetMs ?? PROPOSAL_BATCH_TIME_BUDGET_MS;
32
+ const startedAt = now();
33
+ const { issues, docsById } = await loadOpenFixableIssuesWithDocs(db);
34
+ const eligible = selectEligibleForGeneration(issues, docsById);
35
+ const queue = eligible.slice(0, max);
36
+ // Read once per pass, not once per issue. Fail open: an unreadable AI config
37
+ // leaves every proposal unscored (brandAlignment: null) rather than aborting.
38
+ const brandVoice = await getAIConfig(db)
39
+ .then((c) => c.settings.brandVoice)
40
+ .catch((err) => {
41
+ console.warn('[seo/proposals-generate] AI config unavailable for brand alignment', err);
42
+ return undefined;
43
+ });
44
+ const result = {
45
+ generated: 0,
46
+ skipped: 0,
47
+ failed: 0,
48
+ remaining: 0,
49
+ haltedReason: null,
50
+ };
51
+ let processed = 0;
52
+ let halted = false;
53
+ async function writeMetadata(issueId, patch) {
54
+ const fresh = await db.seoIssue.findUnique({ where: { id: issueId } });
55
+ await db.seoIssue.update({
56
+ where: { id: issueId },
57
+ data: { metadata: { ...asRecord(fresh?.metadata), ...patch } },
58
+ });
59
+ }
60
+ async function processOne(item) {
61
+ const { issue, liveFingerprint } = item;
62
+ // Double-click guard: since our eligibility scan another request may have
63
+ // cached this issue (never spend tokens twice on one document state) or
64
+ // the user may have dismissed it / a prior pass marked it non-cacheable.
65
+ // Writing a cache would clear those sticky markers, so honour them here.
66
+ const fresh = await db.seoIssue.findUnique({ where: { id: issue.id } });
67
+ const freshCache = pendingFixFromMetadata(fresh?.metadata);
68
+ if (freshCache && freshCache.fingerprint === liveFingerprint)
69
+ return 'skipped';
70
+ const sticky = stickyFingerprintsFromMetadata(fresh?.metadata);
71
+ if (sticky.dismissed === liveFingerprint || sticky.generateSkipped === liveFingerprint) {
72
+ return 'skipped';
73
+ }
74
+ const loaded = await loadContext(db, issue);
75
+ if (!loaded.ok) {
76
+ await writeMetadata(issue.id, { generateSkippedFingerprint: liveFingerprint });
77
+ return 'skipped';
78
+ }
79
+ const outcome = await suggest({
80
+ type: issue.type,
81
+ title: issue.title,
82
+ recommendation: issue.recommendation,
83
+ entityType: issue.entityType,
84
+ entityId: issue.entityId,
85
+ }, {
86
+ entity: loaded.ctx.entity,
87
+ siteSeo: loaded.ctx.siteSeo,
88
+ updatedAt: loaded.ctx.updatedAt,
89
+ metadata: asRecord(issue.metadata),
90
+ }, { db, userId: actor.userId, collection: loaded.ctx.collection });
91
+ if (outcome.aiError && HALTING_AI_ERROR_CODES.has(outcome.aiError.code)) {
92
+ result.haltedReason = outcome.aiError.message;
93
+ return 'halted';
94
+ }
95
+ if (outcome.aiError) {
96
+ // Per-issue failure: remember this document state so the next pass moves
97
+ // on instead of retrying the same head-of-queue issue forever.
98
+ await writeMetadata(issue.id, { generateSkippedFingerprint: liveFingerprint });
99
+ return 'failed';
100
+ }
101
+ if (outcome.cacheForApply) {
102
+ await writeMetadata(issue.id, pendingFixToMetadata(outcome.suggestion, {
103
+ brandAlignment: scoreFixBrandAlignment(outcome.suggestion.changes, brandVoice),
104
+ }));
105
+ return 'generated';
106
+ }
107
+ // Nothing cacheable for this document state (deterministic fix covers it,
108
+ // or the type never caches). Remember that so the next pass skips it.
109
+ await writeMetadata(issue.id, { generateSkippedFingerprint: liveFingerprint });
110
+ return 'skipped';
111
+ }
112
+ let cursor = 0;
113
+ async function worker() {
114
+ while (!halted && cursor < queue.length && now() - startedAt < budgetMs) {
115
+ const item = queue[cursor++];
116
+ let outcome;
117
+ try {
118
+ outcome = await processOne(item);
119
+ }
120
+ catch (err) {
121
+ console.warn('[seo/proposals-generate] proposal generation failed', { issueId: item.issue.id }, err);
122
+ outcome = 'failed';
123
+ }
124
+ processed++;
125
+ if (outcome === 'halted') {
126
+ halted = true;
127
+ result.failed++;
128
+ }
129
+ else {
130
+ result[outcome]++;
131
+ }
132
+ }
133
+ }
134
+ await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, worker));
135
+ result.remaining = eligible.length - processed;
136
+ return result;
137
+ }
138
+ //# sourceMappingURL=proposals-generate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposals-generate.js","sourceRoot":"","sources":["../../src/seo/proposals-generate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Unified AI proposal shape over two stores: cached issue fixes
3
+ * (`SeoIssue.metadata.pendingFix`) and open `RedirectSuggestion` rows.
4
+ * Read-only; the generate / bulk-apply services live beside this file.
5
+ */
6
+ import type { PrentaDB } from '../db/client-types.js';
7
+ import { type SeoFixFieldChange } from './issue-fix.js';
8
+ import { type PendingFixCache } from './issue-fix-ai.js';
9
+ export declare const ISSUE_FIX_FEATURE_KEY = "ai.features.seoMetaGeneration";
10
+ export declare const REDIRECT_FEATURE_KEY = "ai.features.seo404Recovery";
11
+ export type SeoProposalSeverity = 'critical' | 'warning' | 'info';
12
+ export interface SeoProposal {
13
+ /** `issue:<issueId>` | `redirect:<suggestionId>` */
14
+ id: string;
15
+ kind: 'issue-fix' | 'redirect';
16
+ title: string;
17
+ entity: {
18
+ type: string | null;
19
+ id: string | null;
20
+ title: string | null;
21
+ url: string | null;
22
+ };
23
+ changes: SeoFixFieldChange[];
24
+ justification: string;
25
+ source: 'ai' | 'deterministic' | 'ai-404-recovery';
26
+ /** 0–1, redirects only. */
27
+ confidence: number | null;
28
+ /** 0–100, issue fixes only. */
29
+ brandAlignment: number | null;
30
+ generatedAt: string | null;
31
+ /** Issue fixes only; redirects are keyed by id. */
32
+ fingerprint: string | null;
33
+ /** Issue fixes: cached fingerprint ≠ live fingerprint. Redirects: always false. */
34
+ stale: boolean;
35
+ issueType: string | null;
36
+ severity: SeoProposalSeverity | null;
37
+ issueId: string | null;
38
+ suggestionId: string | null;
39
+ }
40
+ export interface SeoProposalsPayload {
41
+ proposals: SeoProposal[];
42
+ counts: {
43
+ /** Live (non-stale) issue-fix proposals. */
44
+ issueFixes: number;
45
+ redirects: number;
46
+ stale: number;
47
+ /** Open/recurring + fixable + no live proposal + not sticky-dismissed/skipped. */
48
+ eligibleForGeneration: number;
49
+ };
50
+ /** `allowBulkApply` per feature, so the UI can disable batch approve up front. */
51
+ governance: {
52
+ issueFixBulk: boolean;
53
+ redirectBulk: boolean;
54
+ };
55
+ }
56
+ export interface ProposalDoc {
57
+ id: string;
58
+ updatedAt: Date;
59
+ title: string | null;
60
+ slug: string | null;
61
+ collection: string;
62
+ }
63
+ type IssueRow = Record<string, any>;
64
+ /**
65
+ * Open/recurring issues (any fixability) plus the documents they reference, in
66
+ * two queries. "Reference" covers the issue entity AND, for cached insert-link
67
+ * fixes, the SOURCE document named by `pendingFix.patch.sourceEntityId` — that
68
+ * is the document the fix writes to and the one its fingerprint is built from,
69
+ * so without it `liveFingerprintFor` could never detect a stale link proposal.
70
+ */
71
+ export declare function loadOpenFixableIssuesWithDocs(db: PrentaDB): Promise<{
72
+ issues: IssueRow[];
73
+ docsById: Map<string, ProposalDoc>;
74
+ }>;
75
+ /**
76
+ * The fingerprint a fresh suggestion for this issue would carry right now.
77
+ * Insert-link fixes are fingerprinted on the SOURCE document (the one that
78
+ * gets written), so their live value needs the cache to know the source.
79
+ * `loadOpenFixableIssuesWithDocs` loads sources alongside entities; if one is
80
+ * still missing (deleted since generation) we fall back to the cached value
81
+ * rather than guessing.
82
+ */
83
+ export declare function liveFingerprintFor(issue: IssueRow, cache: PendingFixCache | null, docsById: Map<string, ProposalDoc>): string | null;
84
+ /**
85
+ * The marker `dismiss-fix` stores. After a dismissal there is no cache, so
86
+ * `selectEligibleForGeneration` compares the marker against the ENTITY-based
87
+ * live fingerprint. Insert-link caches are fingerprinted on the source
88
+ * document instead, so for them we store the entity-based value the no-cache
89
+ * path will compute; for every other type the cached fingerprint already is
90
+ * that value.
91
+ */
92
+ export declare function dismissalFingerprintFor(issue: IssueRow, cache: PendingFixCache, entityUpdatedAt: Date | string | null): string;
93
+ export interface EligibleIssue {
94
+ issue: IssueRow;
95
+ liveFingerprint: string;
96
+ }
97
+ /**
98
+ * Issues batch generation should attempt: fixable, open, document present,
99
+ * no LIVE cache (a stale one counts as none), and neither sticky marker
100
+ * equals the live fingerprint. Ordered critical → warning → info.
101
+ */
102
+ export declare function selectEligibleForGeneration(issues: IssueRow[], docsById: Map<string, ProposalDoc>): EligibleIssue[];
103
+ export declare function listSeoProposals(db: PrentaDB): Promise<SeoProposalsPayload>;
104
+ /**
105
+ * Cheap counts for the overview badge: every cached fix (stale ones are still
106
+ * rows in the inbox) + open redirect suggestions. Callers pass the open issues
107
+ * they already loaded so this adds at most one `count` query.
108
+ */
109
+ export declare function countPendingProposals(db: PrentaDB, openIssues: Array<{
110
+ metadata: unknown;
111
+ }>): Promise<{
112
+ issueFixes: number;
113
+ redirects: number;
114
+ }>;
115
+ export {};
116
+ //# sourceMappingURL=proposals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../../src/seo/proposals.ts"],"names":[],"mappings":"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"}