@localizeaso/cli 0.1.0-preview.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 (58) hide show
  1. package/README.md +24 -0
  2. package/package.json +35 -0
  3. package/packages/asc-shared/dist/app-store-review.d.ts +610 -0
  4. package/packages/asc-shared/dist/app-store-review.d.ts.map +1 -0
  5. package/packages/asc-shared/dist/app-store-review.js +242 -0
  6. package/packages/asc-shared/dist/aso-keyword-map.d.ts +94 -0
  7. package/packages/asc-shared/dist/aso-keyword-map.d.ts.map +1 -0
  8. package/packages/asc-shared/dist/aso-keyword-map.js +292 -0
  9. package/packages/asc-shared/dist/constants.d.ts +15 -0
  10. package/packages/asc-shared/dist/constants.d.ts.map +1 -0
  11. package/packages/asc-shared/dist/constants.js +130 -0
  12. package/packages/asc-shared/dist/cross-localization.d.ts +29 -0
  13. package/packages/asc-shared/dist/cross-localization.d.ts.map +1 -0
  14. package/packages/asc-shared/dist/cross-localization.js +189 -0
  15. package/packages/asc-shared/dist/dedupe.d.ts +17 -0
  16. package/packages/asc-shared/dist/dedupe.d.ts.map +1 -0
  17. package/packages/asc-shared/dist/dedupe.js +104 -0
  18. package/packages/asc-shared/dist/design-tokens.d.ts +83 -0
  19. package/packages/asc-shared/dist/design-tokens.d.ts.map +1 -0
  20. package/packages/asc-shared/dist/design-tokens.js +73 -0
  21. package/packages/asc-shared/dist/index.d.ts +16 -0
  22. package/packages/asc-shared/dist/index.d.ts.map +1 -0
  23. package/packages/asc-shared/dist/index.js +16 -0
  24. package/packages/asc-shared/dist/keywords.d.ts +48 -0
  25. package/packages/asc-shared/dist/keywords.d.ts.map +1 -0
  26. package/packages/asc-shared/dist/keywords.js +376 -0
  27. package/packages/asc-shared/dist/limits.d.ts +11 -0
  28. package/packages/asc-shared/dist/limits.d.ts.map +1 -0
  29. package/packages/asc-shared/dist/limits.js +9 -0
  30. package/packages/asc-shared/dist/locales.d.ts +10 -0
  31. package/packages/asc-shared/dist/locales.d.ts.map +1 -0
  32. package/packages/asc-shared/dist/locales.js +314 -0
  33. package/packages/asc-shared/dist/monetization-boundary.d.ts +148 -0
  34. package/packages/asc-shared/dist/monetization-boundary.d.ts.map +1 -0
  35. package/packages/asc-shared/dist/monetization-boundary.js +365 -0
  36. package/packages/asc-shared/dist/post-approval-paths.d.ts +30 -0
  37. package/packages/asc-shared/dist/post-approval-paths.d.ts.map +1 -0
  38. package/packages/asc-shared/dist/post-approval-paths.js +25 -0
  39. package/packages/asc-shared/dist/review-gate-summary.d.ts +166 -0
  40. package/packages/asc-shared/dist/review-gate-summary.d.ts.map +1 -0
  41. package/packages/asc-shared/dist/review-gate-summary.js +354 -0
  42. package/packages/asc-shared/dist/reviewer-feedback.d.ts +19 -0
  43. package/packages/asc-shared/dist/reviewer-feedback.d.ts.map +1 -0
  44. package/packages/asc-shared/dist/reviewer-feedback.js +94 -0
  45. package/packages/asc-shared/dist/screenshot-review.d.ts +478 -0
  46. package/packages/asc-shared/dist/screenshot-review.d.ts.map +1 -0
  47. package/packages/asc-shared/dist/screenshot-review.js +17 -0
  48. package/packages/asc-shared/dist/supabase.types.d.ts +541 -0
  49. package/packages/asc-shared/dist/supabase.types.d.ts.map +1 -0
  50. package/packages/asc-shared/dist/supabase.types.js +5 -0
  51. package/packages/asc-shared/dist/validation.d.ts +42 -0
  52. package/packages/asc-shared/dist/validation.d.ts.map +1 -0
  53. package/packages/asc-shared/dist/validation.js +113 -0
  54. package/scripts/ensure-shared-build.mjs +76 -0
  55. package/scripts/export-astro-mcp-apps.mjs +841 -0
  56. package/scripts/localizeaso.mjs +2100 -0
  57. package/scripts/review-agent.mjs +9092 -0
  58. package/scripts/review-mcp.mjs +5931 -0
@@ -0,0 +1,354 @@
1
+ function normalizedReviewSignalGapCount(value) {
2
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
3
+ }
4
+ export function formatReviewSignalGapSummaryLine(input, options = {}) {
5
+ const missingKeywordMappingCount = normalizedReviewSignalGapCount(input.missingKeywordMappingCount);
6
+ const missingRationaleCount = normalizedReviewSignalGapCount(input.missingRationaleCount);
7
+ const noWarningsReportedCount = normalizedReviewSignalGapCount(input.noWarningsReportedCount);
8
+ const screenshotEvidenceGapCount = normalizedReviewSignalGapCount(input.screenshotEvidenceGapCount);
9
+ const keywordMappingNotApplicable = options.keywordMappingNotApplicable ||
10
+ normalizedReviewSignalGapCount(input.keywordMappingNotApplicableCount) > 0;
11
+ const gapParts = [
12
+ missingKeywordMappingCount
13
+ ? `${missingKeywordMappingCount} missing keyword mapping${missingKeywordMappingCount === 1 ? '' : 's'}`
14
+ : null,
15
+ missingRationaleCount
16
+ ? `${missingRationaleCount} missing rationale${missingRationaleCount === 1 ? '' : 's'}`
17
+ : null,
18
+ noWarningsReportedCount
19
+ ? `${noWarningsReportedCount} target${noWarningsReportedCount === 1 ? '' : 's'} with no warnings reported`
20
+ : null,
21
+ screenshotEvidenceGapCount
22
+ ? `${screenshotEvidenceGapCount} screenshot evidence gap${screenshotEvidenceGapCount === 1 ? '' : 's'}`
23
+ : null,
24
+ ].filter((part) => Boolean(part));
25
+ if (!gapParts.length) {
26
+ if (keywordMappingNotApplicable)
27
+ return 'Signal gaps: none reported; keyword mapping n/a.';
28
+ return options.includeNoGaps === false ? '' : 'Signal gaps: none reported.';
29
+ }
30
+ if (keywordMappingNotApplicable && !missingKeywordMappingCount) {
31
+ return `Signal gaps: ${gapParts.join(', ')}; keyword mapping n/a.`;
32
+ }
33
+ return `Signal gaps: ${gapParts.join(', ')}.`;
34
+ }
35
+ export function normalizeReviewSignalList(values) {
36
+ if (!values?.length)
37
+ return [];
38
+ const seen = new Set();
39
+ const normalized = [];
40
+ for (const value of values) {
41
+ const item = typeof value === 'string' ? value.trim() : '';
42
+ if (!item)
43
+ continue;
44
+ const key = item.toLocaleLowerCase();
45
+ if (seen.has(key))
46
+ continue;
47
+ seen.add(key);
48
+ normalized.push(item);
49
+ }
50
+ return normalized;
51
+ }
52
+ export function reviewSignalCount(values) {
53
+ return normalizeReviewSignalList(values).length;
54
+ }
55
+ export function uniqueReviewSignalCount(values) {
56
+ return normalizeReviewSignalList(values.flatMap((items) => items ?? [])).length;
57
+ }
58
+ export function hasReviewSignals(values) {
59
+ return reviewSignalCount(values) > 0;
60
+ }
61
+ export function reviewSignalAudit(input) {
62
+ const hasAssignedKeywords = hasReviewSignals(input.assignedKeywords);
63
+ const hasUnassignedKeywords = hasReviewSignals(input.unassignedKeywords);
64
+ const hasWarnings = hasReviewSignals(input.warnings);
65
+ const hasRationale = Boolean(input.rationale?.trim());
66
+ const keywordMappingNotApplicable = input.keywordMappingNotApplicable === true;
67
+ const hasKeywordMapping = keywordMappingNotApplicable || hasAssignedKeywords || hasUnassignedKeywords;
68
+ const missingReviewSignals = [
69
+ !hasKeywordMapping ? 'keyword mapping' : null,
70
+ !hasRationale ? 'rationale' : null,
71
+ ].filter((value) => Boolean(value));
72
+ return {
73
+ hasAssignedKeywords,
74
+ hasUnassignedKeywords,
75
+ keywordMappingNotApplicable,
76
+ hasKeywordMapping,
77
+ hasWarnings,
78
+ hasRationale,
79
+ missingReviewSignals,
80
+ noWarningsReported: !hasWarnings,
81
+ needsReviewerAttention: missingReviewSignals.length > 0 || !hasWarnings,
82
+ };
83
+ }
84
+ export function reviewSignalAuditSummary(inputs) {
85
+ let keywordMappingNotApplicableCount = 0;
86
+ let missingKeywordMappingCount = 0;
87
+ let missingRationaleCount = 0;
88
+ let noWarningsReportedCount = 0;
89
+ let targetsNeedingAttentionCount = 0;
90
+ for (const input of inputs) {
91
+ const audit = reviewSignalAudit(input);
92
+ if (audit.keywordMappingNotApplicable)
93
+ keywordMappingNotApplicableCount += 1;
94
+ if (!audit.hasKeywordMapping)
95
+ missingKeywordMappingCount += 1;
96
+ if (!audit.hasRationale)
97
+ missingRationaleCount += 1;
98
+ if (audit.noWarningsReported)
99
+ noWarningsReportedCount += 1;
100
+ if (audit.needsReviewerAttention)
101
+ targetsNeedingAttentionCount += 1;
102
+ }
103
+ return {
104
+ totalTargets: inputs.length,
105
+ keywordMappingNotApplicableCount,
106
+ missingKeywordMappingCount,
107
+ missingRationaleCount,
108
+ noWarningsReportedCount,
109
+ targetsNeedingAttentionCount,
110
+ allTargetsHaveReviewSignals: targetsNeedingAttentionCount === 0,
111
+ };
112
+ }
113
+ export function screenshotReviewSignalAuditInputs(payload) {
114
+ return payload.locales.flatMap((localeProposal) => localeProposal.frames.flatMap((frame) => frame.layers.map((layer) => ({
115
+ assignedKeywords: [
116
+ ...(localeProposal.assignedKeywords ?? []),
117
+ ...(frame.assignedKeywords ?? []),
118
+ ...(layer.assignedKeywords ?? []),
119
+ ],
120
+ unassignedKeywords: [
121
+ ...(localeProposal.unassignedKeywords ?? []),
122
+ ...(frame.unassignedKeywords ?? []),
123
+ ...(layer.unassignedKeywords ?? []),
124
+ ],
125
+ warnings: [
126
+ ...(localeProposal.warnings ?? []),
127
+ ...(frame.warnings ?? []),
128
+ ...(layer.warnings ?? []),
129
+ ],
130
+ rationale: [localeProposal.rationale, frame.rationale, layer.rationale]
131
+ .filter(Boolean)
132
+ .join(' '),
133
+ }))));
134
+ }
135
+ export function screenshotReviewSignalAuditSummary(payload) {
136
+ return reviewSignalAuditSummary(screenshotReviewSignalAuditInputs(payload));
137
+ }
138
+ export function fieldReviewSignalAuditInputs(proposal) {
139
+ return proposal.changes.map((change) => ({
140
+ assignedKeywords: change.assignedKeywords,
141
+ unassignedKeywords: change.unassignedKeywords,
142
+ warnings: change.warnings,
143
+ rationale: change.rationale,
144
+ keywordMappingNotApplicable: change.target.surface === 'pricing',
145
+ }));
146
+ }
147
+ export function fieldReviewSignalAuditSummary(proposal) {
148
+ return reviewSignalAuditSummary(fieldReviewSignalAuditInputs(proposal));
149
+ }
150
+ export function buildReviewGateSummary(params) {
151
+ const audit = params.signalAudit ?? undefined;
152
+ const screenshotEvidence = params.screenshotEvidence ?? undefined;
153
+ const warnings = [];
154
+ const screenshotMissingTargetCount = Math.max(0, Number(screenshotEvidence?.missingTargetCount ?? 0));
155
+ const screenshotFallbackOnlyTargetCount = Math.max(0, Number(screenshotEvidence?.fallbackOnlyTargetCount ?? 0));
156
+ const screenshotContextOnlyTargetCount = Math.max(0, Number(screenshotEvidence?.contextOnlyTargetCount ?? 0));
157
+ const screenshotStrongEvidenceTargetCount = Math.max(0, Number(screenshotEvidence?.strongEvidenceTargetCount ?? 0));
158
+ const screenshotWeakEvidenceTargetCount = Math.max(0, Number(screenshotEvidence?.weakEvidenceTargetCount ?? 0));
159
+ const screenshotEvidenceGapCount = screenshotMissingTargetCount +
160
+ screenshotFallbackOnlyTargetCount +
161
+ screenshotContextOnlyTargetCount +
162
+ screenshotWeakEvidenceTargetCount;
163
+ const targetsNeedingAttentionCount = audit?.targetsNeedingAttentionCount ?? 0;
164
+ const signalGate = screenshotEvidenceGapCount > 0 || targetsNeedingAttentionCount > 0
165
+ ? 'attention_required'
166
+ : audit?.allTargetsHaveReviewSignals === true
167
+ ? 'complete'
168
+ : 'unknown';
169
+ if (params.pendingTargetCount > 0) {
170
+ warnings.push(`${params.pendingTargetCount} target${params.pendingTargetCount === 1 ? '' : 's'} still ${params.pendingTargetCount === 1 ? 'needs' : 'need'} human decisions.`);
171
+ }
172
+ if ((audit?.missingKeywordMappingCount ?? 0) > 0) {
173
+ const count = audit?.missingKeywordMappingCount ?? 0;
174
+ warnings.push(`${count} target${count === 1 ? '' : 's'} ${count === 1 ? 'has' : 'have'} no keyword mapping.`);
175
+ }
176
+ if ((audit?.missingRationaleCount ?? 0) > 0) {
177
+ const count = audit?.missingRationaleCount ?? 0;
178
+ warnings.push(`${count} target${count === 1 ? '' : 's'} ${count === 1 ? 'has' : 'have'} no rationale.`);
179
+ }
180
+ if ((audit?.noWarningsReportedCount ?? 0) > 0) {
181
+ const count = audit?.noWarningsReportedCount ?? 0;
182
+ warnings.push(`${count} target${count === 1 ? '' : 's'} ${count === 1 ? 'has' : 'have'} no warnings reported.`);
183
+ }
184
+ if (screenshotEvidenceGapCount > 0) {
185
+ warnings.push(`${screenshotEvidenceGapCount} field target${screenshotEvidenceGapCount === 1 ? '' : 's'} ${screenshotEvidenceGapCount === 1 ? 'has' : 'have'} missing, fallback-only, context-only, or weak screenshot evidence.`);
186
+ }
187
+ return {
188
+ kind: 'localizeaso_review_readiness_gate',
189
+ reviewKind: params.reviewKind,
190
+ ready: params.ready,
191
+ humanDecisionGate: params.pendingTargetCount === 0 ? 'complete' : 'pending',
192
+ signalGate,
193
+ pendingTargetCount: params.pendingTargetCount,
194
+ targetsNeedingAttentionCount,
195
+ keywordMappingNotApplicableCount: audit?.keywordMappingNotApplicableCount ?? 0,
196
+ missingKeywordMappingCount: audit?.missingKeywordMappingCount ?? 0,
197
+ missingRationaleCount: audit?.missingRationaleCount ?? 0,
198
+ noWarningsReportedCount: audit?.noWarningsReportedCount ?? 0,
199
+ ...(screenshotEvidence
200
+ ? {
201
+ screenshotEvidenceGapCount,
202
+ screenshotMissingTargetCount,
203
+ screenshotFallbackOnlyTargetCount,
204
+ screenshotContextOnlyTargetCount,
205
+ screenshotStrongEvidenceTargetCount,
206
+ screenshotWeakEvidenceTargetCount,
207
+ }
208
+ : {}),
209
+ warnings,
210
+ agentInstruction: 'Readiness only reports review quality and human-decision coverage. It does not approve, apply, mark status, schedule/publish pricing, or publish/submit to App Store Connect.',
211
+ };
212
+ }
213
+ function readinessProtectedActions(reviewKind) {
214
+ if (reviewKind === 'screenshots') {
215
+ return [
216
+ 'human_approval',
217
+ 'review_rejection',
218
+ 'figma_apply',
219
+ 'screenshot_upload',
220
+ 'app_store_submit',
221
+ 'status_update',
222
+ ];
223
+ }
224
+ return [
225
+ 'human_approval',
226
+ 'review_rejection',
227
+ 'metadata_apply',
228
+ 'keyword_apply',
229
+ 'pricing_export',
230
+ 'pricing_schedule',
231
+ 'app_store_submit',
232
+ 'status_update',
233
+ ];
234
+ }
235
+ export function buildReviewReadinessBoundary(params) {
236
+ return {
237
+ kind: 'localizeaso_readiness_boundary',
238
+ reviewKind: params.reviewKind,
239
+ readOnly: true,
240
+ mutatesReviewData: false,
241
+ mutatesAppStoreConnect: false,
242
+ approvalGranted: false,
243
+ postApprovalActionAllowed: false,
244
+ requiresHumanApprovalConsent: true,
245
+ requiresHumanPostApprovalConsentForApplySubmit: true,
246
+ protectedActions: readinessProtectedActions(params.reviewKind),
247
+ nextHumanAction: params.ready
248
+ ? 'Open the human review screen and approve only from the explicit consent workflow.'
249
+ : 'Resolve pending decisions, signal gaps, or final-value blockers before approval.',
250
+ agentInstruction: 'Readiness only reports whether the proposal is approvable. It does not approve, reject, apply, export, schedule, upload, submit, publish, or mark status.',
251
+ };
252
+ }
253
+ function cleanSummaryString(value) {
254
+ return typeof value === 'string' ? value.trim() : '';
255
+ }
256
+ export function summarizeFieldReviewFinalValueBlockingIssue(issue, index = 0) {
257
+ if (!issue || typeof issue !== 'object' || Array.isArray(issue))
258
+ return `Blocking issue ${index + 1}.`;
259
+ const record = issue;
260
+ const code = cleanSummaryString(record.code) || 'FIELD_REVIEW_FINAL_VALUE_BLOCKED';
261
+ const message = cleanSummaryString(record.message);
262
+ const target = record.target && typeof record.target === 'object' && !Array.isArray(record.target)
263
+ ? Object.entries(record.target)
264
+ .map(([key, value]) => {
265
+ const text = cleanSummaryString(value);
266
+ return text ? `${key}=${text}` : null;
267
+ })
268
+ .filter(Boolean)
269
+ .join(', ')
270
+ : '';
271
+ return [code, target ? `target ${target}` : '', message].filter(Boolean).join(': ');
272
+ }
273
+ export function buildFieldReviewFinalValueGate(blockingIssues) {
274
+ return {
275
+ kind: 'localizeaso_field_final_value_gate',
276
+ ready: blockingIssues.length === 0,
277
+ status: blockingIssues.length > 0 ? 'blocked' : 'clear',
278
+ blockingIssueCount: blockingIssues.length,
279
+ warnings: blockingIssues.map((issue, index) => summarizeFieldReviewFinalValueBlockingIssue(issue, index)),
280
+ agentInstruction: blockingIssues.length > 0
281
+ ? 'This field review is not approvable until the human edits the saved decisions or requests a revised proposal that clears every final-value blocker.'
282
+ : 'No field final-value blockers were reported. Readiness still does not approve, apply, schedule/publish pricing, or upload/publish/submit to App Store Connect.',
283
+ };
284
+ }
285
+ export function buildReadinessReviewGate(params) {
286
+ const reviewReady = params.reviewGateSummary?.ready === true || params.ready === true;
287
+ const finalValueBlocked = params.finalValueGate?.status === 'blocked' ||
288
+ (typeof params.finalValueGate?.blockingIssueCount === 'number' && params.finalValueGate.blockingIssueCount > 0);
289
+ const warnings = [
290
+ ...(params.reviewGateSummary?.warnings ?? []),
291
+ ...(params.finalValueGate?.warnings ?? []),
292
+ ].map(cleanSummaryString).filter(Boolean);
293
+ const pendingTargetCount = typeof params.reviewGateSummary?.pendingTargetCount === 'number'
294
+ ? params.reviewGateSummary.pendingTargetCount
295
+ : typeof params.pendingTargetCount === 'number'
296
+ ? params.pendingTargetCount
297
+ : undefined;
298
+ const agentInstruction = cleanSummaryString(params.finalValueGate?.agentInstruction) ||
299
+ cleanSummaryString(params.readinessBoundary?.agentInstruction) ||
300
+ cleanSummaryString(params.reviewGateSummary?.agentInstruction) ||
301
+ 'Readiness is inspection only. It does not approve, apply, export, schedule, submit, or mark status.';
302
+ const screenshotEvidenceGapCount = typeof params.reviewGateSummary?.screenshotEvidenceGapCount === 'number'
303
+ ? params.reviewGateSummary.screenshotEvidenceGapCount
304
+ : undefined;
305
+ const signalGate = screenshotEvidenceGapCount &&
306
+ screenshotEvidenceGapCount > 0 &&
307
+ params.reviewGateSummary?.signalGate !== 'attention_required'
308
+ ? 'attention_required'
309
+ : params.reviewGateSummary?.signalGate;
310
+ return {
311
+ kind: 'localizeaso_readiness_review_gate',
312
+ phase: 'readiness_inspection',
313
+ reviewKind: params.reviewKind,
314
+ ready: reviewReady && !finalValueBlocked,
315
+ humanReviewRequired: true,
316
+ readOnly: true,
317
+ approvalGranted: false,
318
+ postApprovalActionAllowed: false,
319
+ ...(typeof pendingTargetCount === 'number' ? { pendingTargetCount } : {}),
320
+ ...(params.reviewGateSummary?.humanDecisionGate ? { humanDecisionGate: params.reviewGateSummary.humanDecisionGate } : {}),
321
+ ...(signalGate ? { signalGate } : {}),
322
+ ...(typeof params.reviewGateSummary?.keywordMappingNotApplicableCount === 'number'
323
+ ? { keywordMappingNotApplicableCount: params.reviewGateSummary.keywordMappingNotApplicableCount }
324
+ : {}),
325
+ ...(typeof screenshotEvidenceGapCount === 'number' ? { screenshotEvidenceGapCount } : {}),
326
+ ...(typeof params.reviewGateSummary?.screenshotMissingTargetCount === 'number'
327
+ ? { screenshotMissingTargetCount: params.reviewGateSummary.screenshotMissingTargetCount }
328
+ : {}),
329
+ ...(typeof params.reviewGateSummary?.screenshotFallbackOnlyTargetCount === 'number'
330
+ ? { screenshotFallbackOnlyTargetCount: params.reviewGateSummary.screenshotFallbackOnlyTargetCount }
331
+ : {}),
332
+ ...(typeof params.reviewGateSummary?.screenshotContextOnlyTargetCount === 'number'
333
+ ? { screenshotContextOnlyTargetCount: params.reviewGateSummary.screenshotContextOnlyTargetCount }
334
+ : {}),
335
+ ...(typeof params.reviewGateSummary?.screenshotStrongEvidenceTargetCount === 'number'
336
+ ? { screenshotStrongEvidenceTargetCount: params.reviewGateSummary.screenshotStrongEvidenceTargetCount }
337
+ : {}),
338
+ ...(typeof params.reviewGateSummary?.screenshotWeakEvidenceTargetCount === 'number'
339
+ ? { screenshotWeakEvidenceTargetCount: params.reviewGateSummary.screenshotWeakEvidenceTargetCount }
340
+ : {}),
341
+ ...(params.reviewKind === 'field' && params.finalValueGate
342
+ ? {
343
+ finalValueGateStatus: params.finalValueGate.status,
344
+ finalValueBlockingIssueCount: params.finalValueGate.blockingIssueCount,
345
+ }
346
+ : {}),
347
+ warnings,
348
+ nextHumanAction: cleanSummaryString(params.readinessBoundary?.nextHumanAction) ||
349
+ (finalValueBlocked
350
+ ? 'Resolve final-value blockers in the human review UI before approval.'
351
+ : 'Resolve readiness gaps in the human review UI before approval.'),
352
+ agentInstruction,
353
+ };
354
+ }
@@ -0,0 +1,19 @@
1
+ export type ReviewerFeedbackRequest = {
2
+ index: number;
3
+ instructions: string;
4
+ proposalId?: string | null;
5
+ scope: string[];
6
+ contextSnapshot?: string | null;
7
+ raw: string;
8
+ };
9
+ export declare const REVIEWER_FEEDBACK_CONTEXT_SNAPSHOT_MAX_LENGTH = 12000;
10
+ export declare function normalizeReviewerFeedbackInstructions(value: string): string;
11
+ export declare function normalizeReviewerFeedbackContextSnapshot(value?: string | null): string;
12
+ export declare function formatReviewerFeedbackContextSnapshotLine(value?: string | null): string | null;
13
+ export declare function reviewerFeedbackScopeDisplay(scope: string[] | undefined | null): {
14
+ summary: string | null;
15
+ details: string[];
16
+ };
17
+ export declare function parseReviewerFeedbackRequests(value?: string | null): ReviewerFeedbackRequest[];
18
+ export declare function latestReviewerFeedbackRequest(value?: string | null): ReviewerFeedbackRequest | null;
19
+ //# sourceMappingURL=reviewer-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reviewer-feedback.d.ts","sourceRoot":"","sources":["../src/reviewer-feedback.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAMF,eAAO,MAAM,6CAA6C,QAAQ,CAAC;AAInE,wBAAgB,qCAAqC,CAAC,KAAK,EAAE,MAAM,UAElE;AAED,wBAAgB,wCAAwC,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,UAa7E;AAED,wBAAgB,yCAAyC,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,iBAI9E;AAcD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI;;;EAO9E;AAED,wBAAgB,6BAA6B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,uBAAuB,EAAE,CA4C9F;AAED,wBAAgB,6BAA6B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,uBAAuB,GAAG,IAAI,CAGnG"}
@@ -0,0 +1,94 @@
1
+ const REVIEWER_FEEDBACK_PREFIX = /^\s*(?:[-*]\s*)?(?:>\s*)?Reviewer feedback:\s*/i;
2
+ const RELATED_PROPOSAL_PREFIX = /^\s*(?:[-*]\s*)?(?:>\s*)?Related proposal:\s*/i;
3
+ const SCOPE_PREFIX = /^\s*(?:[-*]\s*)?(?:>\s*)?Scope:\s*/i;
4
+ const CONTEXT_SNAPSHOT_PREFIX = /^\s*(?:[-*]\s*)?(?:>\s*)?Context snapshot:\s*/i;
5
+ export const REVIEWER_FEEDBACK_CONTEXT_SNAPSHOT_MAX_LENGTH = 12000;
6
+ const TRUNCATED_CONTEXT_SNAPSHOT_SUFFIX = '\n[LocalizeASO truncated reviewer context snapshot; copy a narrower locale/field/frame selection if more detail is needed.]';
7
+ export function normalizeReviewerFeedbackInstructions(value) {
8
+ return value.replace(/\s+/g, ' ').trim();
9
+ }
10
+ export function normalizeReviewerFeedbackContextSnapshot(value) {
11
+ const normalized = value
12
+ ?.replace(/\r\n/g, '\n')
13
+ .replace(/\r/g, '\n')
14
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '')
15
+ .trim() ?? '';
16
+ if (normalized.length <= REVIEWER_FEEDBACK_CONTEXT_SNAPSHOT_MAX_LENGTH)
17
+ return normalized;
18
+ const maxBodyLength = Math.max(0, REVIEWER_FEEDBACK_CONTEXT_SNAPSHOT_MAX_LENGTH - TRUNCATED_CONTEXT_SNAPSHOT_SUFFIX.length);
19
+ return `${normalized.slice(0, maxBodyLength).trimEnd()}${TRUNCATED_CONTEXT_SNAPSHOT_SUFFIX}`;
20
+ }
21
+ export function formatReviewerFeedbackContextSnapshotLine(value) {
22
+ const normalized = normalizeReviewerFeedbackContextSnapshot(value);
23
+ if (!normalized)
24
+ return null;
25
+ return `Context snapshot: ${JSON.stringify(normalized)}`;
26
+ }
27
+ function parseContextSnapshotLine(line) {
28
+ const rawValue = line.replace(CONTEXT_SNAPSHOT_PREFIX, '').trim();
29
+ if (!rawValue)
30
+ return null;
31
+ try {
32
+ const parsed = JSON.parse(rawValue);
33
+ if (typeof parsed === 'string')
34
+ return normalizeReviewerFeedbackContextSnapshot(parsed);
35
+ }
36
+ catch {
37
+ // Backward-compatible fallback for unquoted context snapshot lines.
38
+ }
39
+ return normalizeReviewerFeedbackContextSnapshot(rawValue);
40
+ }
41
+ export function reviewerFeedbackScopeDisplay(scope) {
42
+ const summaryPrefix = 'Summary:';
43
+ const values = (scope ?? []).map((item) => item.trim()).filter(Boolean);
44
+ const summary = values.find((item) => item.startsWith(summaryPrefix))?.slice(summaryPrefix.length).trim() ?? null;
45
+ const details = values.filter((item) => !item.startsWith(summaryPrefix));
46
+ return { summary, details };
47
+ }
48
+ export function parseReviewerFeedbackRequests(value) {
49
+ if (!value?.trim())
50
+ return [];
51
+ return value
52
+ .split(/\n{2,}/)
53
+ .map((block) => block.trim())
54
+ .filter((block) => REVIEWER_FEEDBACK_PREFIX.test(block))
55
+ .map((block, index) => {
56
+ const lines = block.split('\n').map((line) => line.trim()).filter(Boolean);
57
+ const instructionLines = [];
58
+ const scope = [];
59
+ let proposalId;
60
+ let contextSnapshot;
61
+ for (const [lineIndex, line] of lines.entries()) {
62
+ if (lineIndex === 0) {
63
+ instructionLines.push(line.replace(REVIEWER_FEEDBACK_PREFIX, '').trim());
64
+ continue;
65
+ }
66
+ if (RELATED_PROPOSAL_PREFIX.test(line)) {
67
+ proposalId = line.replace(RELATED_PROPOSAL_PREFIX, '').trim() || null;
68
+ continue;
69
+ }
70
+ if (SCOPE_PREFIX.test(line)) {
71
+ scope.push(line.replace(SCOPE_PREFIX, '').trim());
72
+ continue;
73
+ }
74
+ if (CONTEXT_SNAPSHOT_PREFIX.test(line)) {
75
+ contextSnapshot = parseContextSnapshotLine(line);
76
+ continue;
77
+ }
78
+ instructionLines.push(line);
79
+ }
80
+ return {
81
+ index,
82
+ instructions: instructionLines.join('\n').trim(),
83
+ proposalId,
84
+ scope,
85
+ ...(contextSnapshot ? { contextSnapshot } : {}),
86
+ raw: block,
87
+ };
88
+ })
89
+ .filter((request) => request.instructions.length > 0);
90
+ }
91
+ export function latestReviewerFeedbackRequest(value) {
92
+ const requests = parseReviewerFeedbackRequests(value);
93
+ return requests.length ? requests[requests.length - 1] : null;
94
+ }