@avocadostudio-ai/orchestrator-core 0.1.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/LICENSE +201 -0
- package/dist/agent/agent-context.d.ts +19 -0
- package/dist/agent/agent-context.js +67 -0
- package/dist/agent/agent-logger.d.ts +5 -0
- package/dist/agent/agent-logger.js +22 -0
- package/dist/agent/agent-loop-openai.d.ts +8 -0
- package/dist/agent/agent-loop-openai.js +172 -0
- package/dist/agent/agent-loop.d.ts +56 -0
- package/dist/agent/agent-loop.js +167 -0
- package/dist/agent/agent-provider.d.ts +28 -0
- package/dist/agent/agent-provider.js +63 -0
- package/dist/agent/agent-tools.d.ts +28 -0
- package/dist/agent/agent-tools.js +899 -0
- package/dist/agent/context/editing-guidelines.md +46 -0
- package/dist/agent/context/role.md +39 -0
- package/dist/agent/integration-prompt.d.ts +9 -0
- package/dist/agent/integration-prompt.js +154 -0
- package/dist/agent/sites-agent-context.d.ts +12 -0
- package/dist/agent/sites-agent-context.js +316 -0
- package/dist/agent/sites-agent-shared.d.ts +161 -0
- package/dist/agent/sites-agent-shared.js +1101 -0
- package/dist/agent/sites-agent-tools.d.ts +18 -0
- package/dist/agent/sites-agent-tools.js +1227 -0
- package/dist/chat/anthropic-cache.d.ts +20 -0
- package/dist/chat/anthropic-cache.js +54 -0
- package/dist/chat/anthropic-planner.d.ts +98 -0
- package/dist/chat/anthropic-planner.js +1012 -0
- package/dist/chat/changelog-coverage-validator.d.ts +37 -0
- package/dist/chat/changelog-coverage-validator.js +215 -0
- package/dist/chat/chat-pipeline-context.d.ts +211 -0
- package/dist/chat/chat-pipeline-context.js +249 -0
- package/dist/chat/chat-pipeline-deterministic.d.ts +61 -0
- package/dist/chat/chat-pipeline-deterministic.js +407 -0
- package/dist/chat/chat-pipeline-image.d.ts +86 -0
- package/dist/chat/chat-pipeline-image.js +897 -0
- package/dist/chat/chat-pipeline-shared.d.ts +69 -0
- package/dist/chat/chat-pipeline-shared.js +212 -0
- package/dist/chat/chat-pipeline-translation.d.ts +27 -0
- package/dist/chat/chat-pipeline-translation.js +417 -0
- package/dist/chat/chat-pipeline-ui.d.ts +14 -0
- package/dist/chat/chat-pipeline-ui.js +244 -0
- package/dist/chat/chat-pipeline.d.ts +99 -0
- package/dist/chat/chat-pipeline.js +3999 -0
- package/dist/chat/decomposer.d.ts +21 -0
- package/dist/chat/decomposer.js +65 -0
- package/dist/chat/gemini-planner.d.ts +70 -0
- package/dist/chat/gemini-planner.js +541 -0
- package/dist/chat/hallucination-validator.d.ts +36 -0
- package/dist/chat/hallucination-validator.js +110 -0
- package/dist/chat/locale-strings.d.ts +47 -0
- package/dist/chat/locale-strings.js +100 -0
- package/dist/chat/plan-json-schema.d.ts +133 -0
- package/dist/chat/plan-json-schema.js +112 -0
- package/dist/chat/planner-types.d.ts +120 -0
- package/dist/chat/planner-types.js +66 -0
- package/dist/chat/planner.d.ts +148 -0
- package/dist/chat/planner.js +1361 -0
- package/dist/chat/prompts.d.ts +67 -0
- package/dist/chat/prompts.js +356 -0
- package/dist/chat/provider-routing.d.ts +14 -0
- package/dist/chat/provider-routing.js +27 -0
- package/dist/chat/variation-pipeline.d.ts +135 -0
- package/dist/chat/variation-pipeline.js +837 -0
- package/dist/chat/vision-alt-generator.d.ts +35 -0
- package/dist/chat/vision-alt-generator.js +152 -0
- package/dist/cms/adapter.d.ts +62 -0
- package/dist/cms/adapter.js +1 -0
- package/dist/cms/bootstrap.d.ts +17 -0
- package/dist/cms/bootstrap.js +85 -0
- package/dist/cms/editor-api-adapter.d.ts +23 -0
- package/dist/cms/editor-api-adapter.js +71 -0
- package/dist/cms/index.d.ts +4 -0
- package/dist/cms/index.js +3 -0
- package/dist/cms/json-file-adapter.d.ts +11 -0
- package/dist/cms/json-file-adapter.js +62 -0
- package/dist/demo-mode.d.ts +59 -0
- package/dist/demo-mode.js +201 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +129 -0
- package/dist/http/chat-stream-resumable.d.ts +108 -0
- package/dist/http/chat-stream-resumable.js +290 -0
- package/dist/http/chat-stream.d.ts +99 -0
- package/dist/http/chat-stream.js +92 -0
- package/dist/image/gdrive-client.d.ts +22 -0
- package/dist/image/gdrive-client.js +215 -0
- package/dist/image/image-helpers.d.ts +95 -0
- package/dist/image/image-helpers.js +488 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira/jira-approval.d.ts +22 -0
- package/dist/jira/jira-approval.js +51 -0
- package/dist/jira/jira-client.d.ts +44 -0
- package/dist/jira/jira-client.js +313 -0
- package/dist/jira/jira-poller.d.ts +46 -0
- package/dist/jira/jira-poller.js +184 -0
- package/dist/jira/jira-processor.d.ts +103 -0
- package/dist/jira/jira-processor.js +1085 -0
- package/dist/jira/jira-types.d.ts +117 -0
- package/dist/jira/jira-types.js +38 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +28 -0
- package/dist/migration/mcp-server-stdio.d.ts +8 -0
- package/dist/migration/mcp-server-stdio.js +672 -0
- package/dist/migration/migration-prompt.d.ts +7 -0
- package/dist/migration/migration-prompt.js +197 -0
- package/dist/migration/migration-tools.d.ts +17 -0
- package/dist/migration/migration-tools.js +159 -0
- package/dist/migration/scrape-cache.d.ts +9 -0
- package/dist/migration/scrape-cache.js +19 -0
- package/dist/nlp/deterministic-planner-context.d.ts +141 -0
- package/dist/nlp/deterministic-planner-context.js +362 -0
- package/dist/nlp/deterministic-planner-pages.d.ts +26 -0
- package/dist/nlp/deterministic-planner-pages.js +170 -0
- package/dist/nlp/deterministic-planner-patches.d.ts +80 -0
- package/dist/nlp/deterministic-planner-patches.js +508 -0
- package/dist/nlp/deterministic-planner-refs.d.ts +33 -0
- package/dist/nlp/deterministic-planner-refs.js +164 -0
- package/dist/nlp/deterministic-planner-suggestions.d.ts +49 -0
- package/dist/nlp/deterministic-planner-suggestions.js +579 -0
- package/dist/nlp/deterministic-planner.d.ts +85 -0
- package/dist/nlp/deterministic-planner.js +1631 -0
- package/dist/nlp/intent-detection.d.ts +309 -0
- package/dist/nlp/intent-detection.js +730 -0
- package/dist/nlp/intent-helpers.d.ts +15 -0
- package/dist/nlp/intent-helpers.js +243 -0
- package/dist/nlp/intent-patterns.d.ts +40 -0
- package/dist/nlp/intent-patterns.js +223 -0
- package/dist/nlp/plan-normalizer.d.ts +41 -0
- package/dist/nlp/plan-normalizer.js +1537 -0
- package/dist/ops/destructive-action-gate.d.ts +44 -0
- package/dist/ops/destructive-action-gate.js +90 -0
- package/dist/ops/ops-engine.d.ts +151 -0
- package/dist/ops/ops-engine.js +1394 -0
- package/dist/publish/diff-engine.d.ts +18 -0
- package/dist/publish/diff-engine.js +305 -0
- package/dist/publish/publish-helpers.d.ts +87 -0
- package/dist/publish/publish-helpers.js +521 -0
- package/dist/publish/publish-target-registry.d.ts +7 -0
- package/dist/publish/publish-target-registry.js +61 -0
- package/dist/publish/publish-target.d.ts +81 -0
- package/dist/publish/publish-target.js +1 -0
- package/dist/publish/targets/deploy-hook.d.ts +13 -0
- package/dist/publish/targets/deploy-hook.js +123 -0
- package/dist/publish/targets/git.d.ts +13 -0
- package/dist/publish/targets/git.js +55 -0
- package/dist/publish/targets/site-contract.d.ts +19 -0
- package/dist/publish/targets/site-contract.js +124 -0
- package/dist/state/content-source.d.ts +17 -0
- package/dist/state/content-source.js +1 -0
- package/dist/state/in-memory-content-source.d.ts +27 -0
- package/dist/state/in-memory-content-source.js +51 -0
- package/dist/state/session-lock.d.ts +13 -0
- package/dist/state/session-lock.js +29 -0
- package/dist/state/session-state.d.ts +310 -0
- package/dist/state/session-state.js +1083 -0
- package/dist/state/sqlite-store-singleton.d.ts +31 -0
- package/dist/state/sqlite-store-singleton.js +170 -0
- package/dist/state/sqlite-store.d.ts +135 -0
- package/dist/state/sqlite-store.js +421 -0
- package/dist/telemetry/chat-telemetry.d.ts +105 -0
- package/dist/telemetry/chat-telemetry.js +247 -0
- package/dist/telemetry/eval-candidate-store.d.ts +50 -0
- package/dist/telemetry/eval-candidate-store.js +120 -0
- package/dist/telemetry/feedback-store.d.ts +34 -0
- package/dist/telemetry/feedback-store.js +76 -0
- package/dist/telemetry/jira-telemetry.d.ts +57 -0
- package/dist/telemetry/jira-telemetry.js +68 -0
- package/dist/telemetry/migration-telemetry.d.ts +35 -0
- package/dist/telemetry/migration-telemetry.js +40 -0
- package/dist/telemetry/usage.d.ts +24 -0
- package/dist/telemetry/usage.js +80 -0
- package/dist/tools/builtin-registrations.d.ts +12 -0
- package/dist/tools/builtin-registrations.js +33 -0
- package/dist/tools/builtins/gdrive-browse.d.ts +3 -0
- package/dist/tools/builtins/gdrive-browse.js +68 -0
- package/dist/tools/builtins/image-generate.d.ts +3 -0
- package/dist/tools/builtins/image-generate.js +211 -0
- package/dist/tools/builtins/unsplash-get-by-id.d.ts +23 -0
- package/dist/tools/builtins/unsplash-get-by-id.js +119 -0
- package/dist/tools/builtins/unsplash-search.d.ts +3 -0
- package/dist/tools/builtins/unsplash-search.js +74 -0
- package/dist/tools/executor.d.ts +23 -0
- package/dist/tools/executor.js +169 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/registry.d.ts +21 -0
- package/dist/tools/registry.js +75 -0
- package/dist/tools/runtime.d.ts +27 -0
- package/dist/tools/runtime.js +48 -0
- package/dist/tools/schema-validator.d.ts +24 -0
- package/dist/tools/schema-validator.js +88 -0
- package/dist/tools/types.d.ts +86 -0
- package/dist/tools/types.js +1 -0
- package/dist/variation-images.d.ts +19 -0
- package/dist/variation-images.js +12 -0
- package/package.json +78 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { getAllBlockMeta } from "@avocadostudio-ai/shared";
|
|
2
|
+
export function sanitizeMessageForPlanning(message) {
|
|
3
|
+
const normalized = normalizeVariationTypos(message.replace(/\r\n?/g, "\n")).trim();
|
|
4
|
+
if (normalized.length === 0)
|
|
5
|
+
return normalized;
|
|
6
|
+
const hasDebugEcho = /(^|\n)\s*debug\s*$|(^|\n)\s*(traceid|prompthash|outcome|intent|opcount|ops)\s*:/im.test(normalized);
|
|
7
|
+
const canonicalized = normalized
|
|
8
|
+
// Normalize common smart quote variants so downstream quoted-text parsing is stable.
|
|
9
|
+
.replace(/[\u2018\u2019\u2032]/g, "'")
|
|
10
|
+
.replace(/[\u201C\u201D\u2033]/g, "\"")
|
|
11
|
+
// Light typo normalization for frequently used edit verbs/nouns.
|
|
12
|
+
.replace(/\btestomonials\b/gi, "testimonials")
|
|
13
|
+
.replace(/\bfetures\b/gi, "features")
|
|
14
|
+
.replace(/\bheding\b/gi, "heading")
|
|
15
|
+
.replace(/\bad a\b/gi, "add a")
|
|
16
|
+
.replace(/\bupdte\b/gi, "update")
|
|
17
|
+
.replace(/\btras?n[asl]{1,3}[aet]{1,3}e?\b/gi, (m) => /translat/i.test(m) ? m : "translate");
|
|
18
|
+
if (!hasDebugEcho)
|
|
19
|
+
return canonicalized;
|
|
20
|
+
const promptEcho = canonicalized.match(/(^|\n)\s*prompt\s*:\s*(.+)$/im)?.[2]?.trim();
|
|
21
|
+
if (promptEcho && promptEcho.length > 0)
|
|
22
|
+
return promptEcho;
|
|
23
|
+
const cleanedLines = canonicalized
|
|
24
|
+
.split("\n")
|
|
25
|
+
.map((line) => line.trimEnd())
|
|
26
|
+
.filter((line) => {
|
|
27
|
+
if (line.trim().length === 0)
|
|
28
|
+
return true;
|
|
29
|
+
if (/^\s*debug\s*$/i.test(line))
|
|
30
|
+
return false;
|
|
31
|
+
if (/^\s*(traceid|prompthash|outcome|intent|opcount|optypes|ops|reason|reasoncategory)\s*:/i.test(line))
|
|
32
|
+
return false;
|
|
33
|
+
if (/^\s*renamed the hero secondary cta\.?\s*$/i.test(line))
|
|
34
|
+
return false;
|
|
35
|
+
if (/^\s*performance awareness\b/i.test(line))
|
|
36
|
+
return false;
|
|
37
|
+
if (/semantic relevance and supports seo,\s*accessibility,\s*and conversion checks\.?\s*$/i.test(line))
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
return cleanedLines.join("\n").trim();
|
|
42
|
+
}
|
|
43
|
+
export function inferTranslationScopeFromMessage(message) {
|
|
44
|
+
const lower = message.toLowerCase();
|
|
45
|
+
const isTranslation = /\btranslate\b/.test(lower) ||
|
|
46
|
+
/\btranslation\b/.test(lower) ||
|
|
47
|
+
/\blocaliz/.test(lower) ||
|
|
48
|
+
/\bgerman\b/.test(lower) ||
|
|
49
|
+
/\bdeutsch\b/.test(lower);
|
|
50
|
+
if (!isTranslation)
|
|
51
|
+
return "none";
|
|
52
|
+
const pageScope = /\b(this|the|entire|whole|full)\s+page\b/.test(lower) ||
|
|
53
|
+
/\bwhole\s+site\b/.test(lower) ||
|
|
54
|
+
/\ball\s+sections?\b/.test(lower) ||
|
|
55
|
+
/\btranslate\s+page\b/.test(lower);
|
|
56
|
+
if (pageScope)
|
|
57
|
+
return "page";
|
|
58
|
+
const componentScope = /\b(this|selected|current)\s+(block|section|component)\b/.test(lower) ||
|
|
59
|
+
/\btranslate\s+(the\s+)?(block|section|component)\b/.test(lower) ||
|
|
60
|
+
/\bselected\s+component\b/.test(lower);
|
|
61
|
+
if (componentScope)
|
|
62
|
+
return "component";
|
|
63
|
+
// Default translation intent to page scope unless the user explicitly narrows it.
|
|
64
|
+
return "page";
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Clone-and-translate compound detection
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
//
|
|
70
|
+
// "clone the home page into /test, same content but translated to Spanish" is a
|
|
71
|
+
// two-phase request the single-pass planner can't satisfy: it can emit
|
|
72
|
+
// `duplicate_page`, but the copy's blocks don't exist yet at plan time, so it
|
|
73
|
+
// can't target them for translation — it silently drops the translation half
|
|
74
|
+
// and narrates a translation that never happens. The pipeline handles this by
|
|
75
|
+
// detecting the intent here, duplicating the page, then translating the fresh
|
|
76
|
+
// copy through the normal full-page translation path.
|
|
77
|
+
// Language synonym → canonical English name used in the follow-up
|
|
78
|
+
// "translate this page to <Lang>" message. Keys are matched lowercase.
|
|
79
|
+
const TRANSLATION_LANGUAGE_NAMES = {
|
|
80
|
+
spanish: "Spanish", español: "Spanish", espanol: "Spanish", castellano: "Spanish",
|
|
81
|
+
german: "German", deutsch: "German",
|
|
82
|
+
french: "French", français: "French", francais: "French",
|
|
83
|
+
italian: "Italian", italiano: "Italian",
|
|
84
|
+
portuguese: "Portuguese", português: "Portuguese", portugues: "Portuguese", brazilian: "Portuguese",
|
|
85
|
+
dutch: "Dutch", nederlands: "Dutch",
|
|
86
|
+
english: "English", inglés: "English", ingles: "English",
|
|
87
|
+
japanese: "Japanese", chinese: "Chinese", mandarin: "Chinese", cantonese: "Chinese",
|
|
88
|
+
korean: "Korean", russian: "Russian", arabic: "Arabic", hindi: "Hindi",
|
|
89
|
+
polish: "Polish", swedish: "Swedish", norwegian: "Norwegian", danish: "Danish",
|
|
90
|
+
finnish: "Finnish", greek: "Greek", turkish: "Turkish", czech: "Czech",
|
|
91
|
+
ukrainian: "Ukrainian", romanian: "Romanian", hungarian: "Hungarian",
|
|
92
|
+
catalan: "Catalan", hebrew: "Hebrew", thai: "Thai", vietnamese: "Vietnamese",
|
|
93
|
+
indonesian: "Indonesian", malay: "Malay", tagalog: "Tagalog", filipino: "Tagalog",
|
|
94
|
+
};
|
|
95
|
+
// clone/duplicate/copy/replicate verb (whole-page scope is enforced separately).
|
|
96
|
+
const CLONE_PAGE_VERB = /\b(?:clone|duplicate|replicate|copy)\b|\bmake\s+(?:a\s+)?(?:copy|duplicate|clone)\b/i;
|
|
97
|
+
// Canonical English name → ISO 639-1 code, used to tag the cloned page's title
|
|
98
|
+
// (e.g. "Home (ES)") so a localized copy is distinguishable from its source.
|
|
99
|
+
const LANGUAGE_ISO_CODES = {
|
|
100
|
+
Spanish: "ES", German: "DE", French: "FR", Italian: "IT", Portuguese: "PT",
|
|
101
|
+
Dutch: "NL", English: "EN", Japanese: "JA", Chinese: "ZH", Korean: "KO",
|
|
102
|
+
Russian: "RU", Arabic: "AR", Hindi: "HI", Polish: "PL", Swedish: "SV",
|
|
103
|
+
Norwegian: "NO", Danish: "DA", Finnish: "FI", Greek: "EL", Turkish: "TR",
|
|
104
|
+
Czech: "CS", Ukrainian: "UK", Romanian: "RO", Hungarian: "HU", Catalan: "CA",
|
|
105
|
+
Hebrew: "HE", Thai: "TH", Vietnamese: "VI", Indonesian: "ID", Malay: "MS", Tagalog: "TL",
|
|
106
|
+
};
|
|
107
|
+
export function parseCloneAndTranslateRequest(message) {
|
|
108
|
+
const text = message.replace(/\s+/g, " ").trim();
|
|
109
|
+
const lower = text.toLowerCase();
|
|
110
|
+
if (!CLONE_PAGE_VERB.test(lower))
|
|
111
|
+
return null;
|
|
112
|
+
// Must be a whole-page clone, not a block/section copy ("duplicate this card",
|
|
113
|
+
// "copy the hero"). Require a page noun or a route, and bail if it clearly
|
|
114
|
+
// targets a block instead of a page.
|
|
115
|
+
const mentionsPage = /\b(?:page|homepage|home\s*page|landing|site)\b/.test(lower) || /\/[a-z0-9]/i.test(text);
|
|
116
|
+
if (!mentionsPage)
|
|
117
|
+
return null;
|
|
118
|
+
if (/\b(?:this|the|that)\s+(?:block|section|card|hero|cta|grid|component|banner|testimonial)\b/.test(lower) && !/\bpage\b/.test(lower)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const langAlternation = Object.keys(TRANSLATION_LANGUAGE_NAMES).join("|");
|
|
122
|
+
// Prefer a language introduced by an explicit translate/localize verb so the
|
|
123
|
+
// looser prepositional fallback can't misfire.
|
|
124
|
+
const explicitTranslate = new RegExp(`\\b(?:translat\\w*|localiz\\w*|render(?:ed)?|convert(?:ed)?)\\b[^.]*?\\b(?:in|into|to)\\s+(${langAlternation})\\b`, "i").exec(lower);
|
|
125
|
+
const prepositional = new RegExp(`\\b(?:in|into|to)\\s+(${langAlternation})\\b`, "i").exec(lower);
|
|
126
|
+
const langKey = (explicitTranslate?.[1] ?? prepositional?.[1])?.toLowerCase();
|
|
127
|
+
if (!langKey)
|
|
128
|
+
return null;
|
|
129
|
+
// For the loose prepositional match only ("… in Spanish", no translate verb),
|
|
130
|
+
// guard against the language naming something OTHER than a translation target:
|
|
131
|
+
// - a SOURCE descriptor: "clone the Spanish page"
|
|
132
|
+
// - a STYLING request: "copy the page in a Spanish style", "in French restaurant
|
|
133
|
+
// theme" — the user wants the page restyled, not machine-translated.
|
|
134
|
+
// Match the language followed (within ≤2 words, no clause break) by a source or
|
|
135
|
+
// styling noun. A false negative here just defers to the LLM (safe); a false
|
|
136
|
+
// positive silently translates a page the user wanted restyled (not safe).
|
|
137
|
+
const sourceOrStyleNoun = "page|version|site|homepage|style|theme|design|aesthetics?|branding|brand|look|feel|vibe|mood|fonts?|colou?rs?|palette|layout|tone";
|
|
138
|
+
if (!explicitTranslate && new RegExp(`\\b${langKey}\\b(?:\\s+\\w+){0,2}\\s+(?:${sourceOrStyleNoun})\\b`, "i").test(lower)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const languageName = TRANSLATION_LANGUAGE_NAMES[langKey];
|
|
142
|
+
if (!languageName)
|
|
143
|
+
return null;
|
|
144
|
+
// Optional explicit target slug: an explicit route after into/to/as/at/called.
|
|
145
|
+
let newSlug;
|
|
146
|
+
const routeMatch = text.match(/\b(?:into|to|as|at|named|called)\s+(?:an?\s+|the\s+)?(?:brand[\s-]?new\s+|new\s+)?(?:page\s+(?:called\s+|named\s+|at\s+)?)?["'`]?(\/[a-z0-9][a-z0-9-]*)/i);
|
|
147
|
+
if (routeMatch) {
|
|
148
|
+
newSlug = routeMatch[1].toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Capture the page name after "called/named", stopping before any trailing
|
|
152
|
+
// clause ("… translated to Spanish", "… and …") so we don't slurp it in.
|
|
153
|
+
const namedMatch = text.match(/\b(?:called|named)\s+["'`]?([a-z0-9][a-z0-9 -]*?)["'`]?\s*(?:,|\bpage\b|\btranslat\w*|\blocaliz\w*|\band\b|\bbut\b|\bwith\b|\bin\b|\bto\b|$)/i);
|
|
154
|
+
const name = namedMatch?.[1]?.trim();
|
|
155
|
+
if (name) {
|
|
156
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
157
|
+
if (slug)
|
|
158
|
+
newSlug = `/${slug}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { newSlug, languageName, languageCode: LANGUAGE_ISO_CODES[languageName] ?? languageName.slice(0, 2).toUpperCase() };
|
|
162
|
+
}
|
|
163
|
+
export function isNonEmptyString(value) {
|
|
164
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
165
|
+
}
|
|
166
|
+
function normalizeUpdatePropsPatch(op) {
|
|
167
|
+
const rawPatch = op.patch;
|
|
168
|
+
if (rawPatch &&
|
|
169
|
+
typeof rawPatch.props === "object" &&
|
|
170
|
+
rawPatch.props !== null &&
|
|
171
|
+
!Array.isArray(rawPatch.props)) {
|
|
172
|
+
return rawPatch.props;
|
|
173
|
+
}
|
|
174
|
+
return rawPatch;
|
|
175
|
+
}
|
|
176
|
+
function isExplicitMultiTargetCtaRequest(message) {
|
|
177
|
+
const lower = message.toLowerCase();
|
|
178
|
+
const mentionsCta = /\bcta\b/.test(lower);
|
|
179
|
+
const mentionsHeroTarget = /\bhero\b/.test(lower);
|
|
180
|
+
const mentionsFooterTarget = /\bfooter\b/.test(lower) || /\bcta section\b/.test(lower);
|
|
181
|
+
const asksMultipleTargets = /\b(both|all|each)\b/.test(lower) || (mentionsHeroTarget && mentionsFooterTarget);
|
|
182
|
+
return mentionsCta && asksMultipleTargets && mentionsHeroTarget && mentionsFooterTarget;
|
|
183
|
+
}
|
|
184
|
+
function shouldKeepLinksUnchanged(message) {
|
|
185
|
+
const lower = message.toLowerCase();
|
|
186
|
+
return (/\bkeep\b[^.\n]{0,80}\b(?:links?|hrefs?|urls?)\b[^.\n]{0,80}\bunchanged\b/.test(lower) ||
|
|
187
|
+
/\b(?:without|don't|do not)\b[^.\n]{0,80}\bchange\b[^.\n]{0,80}\b(?:links?|hrefs?|urls?)\b/.test(lower));
|
|
188
|
+
}
|
|
189
|
+
export function findExplicitCtaTargetCoverageGap(args) {
|
|
190
|
+
if (!isExplicitMultiTargetCtaRequest(args.message))
|
|
191
|
+
return null;
|
|
192
|
+
if (args.plan.intent !== "edit_plan")
|
|
193
|
+
return null;
|
|
194
|
+
const hero = args.currentPage.blocks.find((block) => block.type === "Hero");
|
|
195
|
+
const footerCta = args.currentPage.blocks.find((block) => block.type === "CTA");
|
|
196
|
+
if (!hero || !footerCta)
|
|
197
|
+
return null;
|
|
198
|
+
const heroOps = args.plan.ops.filter((op) => op.op === "update_props" && op.pageSlug === args.slug && op.blockId === hero.id);
|
|
199
|
+
const footerOps = args.plan.ops.filter((op) => op.op === "update_props" && op.pageSlug === args.slug && op.blockId === footerCta.id);
|
|
200
|
+
const heroCurrentCta = String(hero.props.ctaText ?? "");
|
|
201
|
+
const footerCurrentCta = String(footerCta.props.ctaText ?? "");
|
|
202
|
+
const heroCovered = heroOps.some((op) => {
|
|
203
|
+
const patch = normalizeUpdatePropsPatch(op);
|
|
204
|
+
const next = patch.ctaText;
|
|
205
|
+
return typeof next === "string" && next.trim().length > 0 && next.trim() !== heroCurrentCta.trim();
|
|
206
|
+
});
|
|
207
|
+
const footerCovered = footerOps.some((op) => {
|
|
208
|
+
const patch = normalizeUpdatePropsPatch(op);
|
|
209
|
+
const next = patch.ctaText;
|
|
210
|
+
return typeof next === "string" && next.trim().length > 0 && next.trim() !== footerCurrentCta.trim();
|
|
211
|
+
});
|
|
212
|
+
const missingTargets = [];
|
|
213
|
+
if (!heroCovered)
|
|
214
|
+
missingTargets.push("hero CTA text");
|
|
215
|
+
if (!footerCovered)
|
|
216
|
+
missingTargets.push("footer CTA text");
|
|
217
|
+
if (missingTargets.length > 0) {
|
|
218
|
+
return `Invalid explicit CTA target coverage. Missing updates for: ${missingTargets.join(", ")}.`;
|
|
219
|
+
}
|
|
220
|
+
if (!shouldKeepLinksUnchanged(args.message))
|
|
221
|
+
return null;
|
|
222
|
+
const linkChanged = [...heroOps, ...footerOps].some((op) => {
|
|
223
|
+
const patch = normalizeUpdatePropsPatch(op);
|
|
224
|
+
return Object.prototype.hasOwnProperty.call(patch, "ctaHref");
|
|
225
|
+
});
|
|
226
|
+
if (linkChanged) {
|
|
227
|
+
return "Invalid explicit CTA target coverage. Request says to keep links unchanged, but plan modifies CTA links.";
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
export function findFullPageTranslationCoverageGap(args) {
|
|
232
|
+
if (inferTranslationScopeFromMessage(args.message) !== "page")
|
|
233
|
+
return null;
|
|
234
|
+
if (args.plan.intent !== "edit_plan")
|
|
235
|
+
return null;
|
|
236
|
+
const blockMeta = getAllBlockMeta();
|
|
237
|
+
const touchedBlockIds = new Set(args.plan.ops
|
|
238
|
+
.filter((op) => {
|
|
239
|
+
if (!(op.op === "update_props" || op.op === "update_item" || op.op === "add_item" || op.op === "remove_item" || op.op === "move_item"))
|
|
240
|
+
return false;
|
|
241
|
+
return "pageSlug" in op && op.pageSlug === args.slug;
|
|
242
|
+
})
|
|
243
|
+
.map((op) => ("blockId" in op ? op.blockId : ""))
|
|
244
|
+
.filter((id) => typeof id === "string" && id.length > 0));
|
|
245
|
+
if (touchedBlockIds.size === 0)
|
|
246
|
+
return null;
|
|
247
|
+
const missingPaths = [];
|
|
248
|
+
for (const block of args.currentPage.blocks) {
|
|
249
|
+
if (!touchedBlockIds.has(block.id))
|
|
250
|
+
continue;
|
|
251
|
+
const meta = blockMeta[block.type];
|
|
252
|
+
// --- Top-level scalar prop coverage ---
|
|
253
|
+
if (meta?.fields) {
|
|
254
|
+
const translatableTopLevel = Object.entries(meta.fields)
|
|
255
|
+
.filter(([, fm]) => fm.kind === "text" || fm.kind === "richtext" || fm.kind === "imageAlt")
|
|
256
|
+
.map(([key]) => key);
|
|
257
|
+
if (translatableTopLevel.length > 0) {
|
|
258
|
+
const coveredTopLevel = new Set();
|
|
259
|
+
for (const op of args.plan.ops) {
|
|
260
|
+
if (!("pageSlug" in op) || op.pageSlug !== args.slug)
|
|
261
|
+
continue;
|
|
262
|
+
if (op.op === "update_props" && op.blockId === block.id) {
|
|
263
|
+
const patch = normalizeUpdatePropsPatch(op);
|
|
264
|
+
for (const key of translatableTopLevel) {
|
|
265
|
+
if (isNonEmptyString(patch[key]))
|
|
266
|
+
coveredTopLevel.add(key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const blockProps = block.props;
|
|
271
|
+
for (const key of translatableTopLevel) {
|
|
272
|
+
if (!isNonEmptyString(blockProps[key]))
|
|
273
|
+
continue;
|
|
274
|
+
if (!coveredTopLevel.has(key))
|
|
275
|
+
missingPaths.push(`${block.id}.${key}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// --- List field child coverage ---
|
|
280
|
+
const listFields = meta?.listFields ?? {};
|
|
281
|
+
const listEntries = Object.entries(listFields);
|
|
282
|
+
for (const [listKey, listMeta] of listEntries) {
|
|
283
|
+
const translatableItemFields = Object.entries(listMeta.itemFields ?? {})
|
|
284
|
+
.filter(([, fieldMeta]) => fieldMeta.kind === "text" || fieldMeta.kind === "richtext" || fieldMeta.kind === "imageAlt")
|
|
285
|
+
.map(([key]) => key);
|
|
286
|
+
if (translatableItemFields.length === 0)
|
|
287
|
+
continue;
|
|
288
|
+
const listValue = block.props[listKey];
|
|
289
|
+
if (!Array.isArray(listValue) || listValue.length === 0)
|
|
290
|
+
continue;
|
|
291
|
+
const perItemCoverage = new Map();
|
|
292
|
+
const ensureCoverage = (index) => {
|
|
293
|
+
const existing = perItemCoverage.get(index);
|
|
294
|
+
if (existing)
|
|
295
|
+
return existing;
|
|
296
|
+
const next = new Set();
|
|
297
|
+
perItemCoverage.set(index, next);
|
|
298
|
+
return next;
|
|
299
|
+
};
|
|
300
|
+
for (const op of args.plan.ops) {
|
|
301
|
+
if (!("pageSlug" in op) || op.pageSlug !== args.slug)
|
|
302
|
+
continue;
|
|
303
|
+
if (op.op === "update_item" && op.blockId === block.id && op.listKey === listKey) {
|
|
304
|
+
const patch = op.patch;
|
|
305
|
+
// The op may address its target by stable itemId or positional index.
|
|
306
|
+
const targetIdx = typeof op.index === "number"
|
|
307
|
+
? op.index
|
|
308
|
+
: listValue.findIndex((e) => e != null && typeof e === "object" && !Array.isArray(e) && e.id === op.itemId);
|
|
309
|
+
if (targetIdx < 0)
|
|
310
|
+
continue;
|
|
311
|
+
const cov = ensureCoverage(targetIdx);
|
|
312
|
+
for (const key of translatableItemFields) {
|
|
313
|
+
if (isNonEmptyString(patch[key]))
|
|
314
|
+
cov.add(key);
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (op.op === "update_props" && op.blockId === block.id) {
|
|
319
|
+
const patch = op.patch;
|
|
320
|
+
if (!Array.isArray(patch[listKey]))
|
|
321
|
+
continue;
|
|
322
|
+
const rows = patch[listKey];
|
|
323
|
+
for (let idx = 0; idx < rows.length; idx += 1) {
|
|
324
|
+
const rowPatch = rows[idx];
|
|
325
|
+
if (!rowPatch || typeof rowPatch !== "object" || Array.isArray(rowPatch))
|
|
326
|
+
continue;
|
|
327
|
+
const row = rowPatch;
|
|
328
|
+
const cov = ensureCoverage(idx);
|
|
329
|
+
for (const key of translatableItemFields) {
|
|
330
|
+
if (isNonEmptyString(row[key]))
|
|
331
|
+
cov.add(key);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
for (let idx = 0; idx < listValue.length; idx += 1) {
|
|
337
|
+
const item = listValue[idx];
|
|
338
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
339
|
+
continue;
|
|
340
|
+
const row = item;
|
|
341
|
+
const required = translatableItemFields.filter((key) => isNonEmptyString(row[key]));
|
|
342
|
+
if (required.length === 0)
|
|
343
|
+
continue;
|
|
344
|
+
const covered = perItemCoverage.get(idx) ?? new Set();
|
|
345
|
+
const missing = required.filter((key) => !covered.has(key));
|
|
346
|
+
for (const field of missing)
|
|
347
|
+
missingPaths.push(`${block.id}.${listKey}[${idx}].${field}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (missingPaths.length === 0)
|
|
352
|
+
return null;
|
|
353
|
+
// Auto-patch: fill missing fields with original (untranslated) values rather than failing.
|
|
354
|
+
// This ensures partial LLM translations still apply — untranslated text is better than no translation.
|
|
355
|
+
let patched = 0;
|
|
356
|
+
for (const path of missingPaths) {
|
|
357
|
+
// Parse path: "b_hero_1.heading" or "b_cardgrid_7.cards[0].imageAlt"
|
|
358
|
+
const topMatch = path.match(/^([^.]+)\.([^[]+)$/);
|
|
359
|
+
const listMatch = path.match(/^([^.]+)\.([^[]+)\[(\d+)\]\.(.+)$/);
|
|
360
|
+
if (topMatch) {
|
|
361
|
+
const [, blockId, key] = topMatch;
|
|
362
|
+
const block = args.currentPage.blocks.find(b => b.id === blockId);
|
|
363
|
+
if (!block)
|
|
364
|
+
continue;
|
|
365
|
+
const originalValue = block.props[key];
|
|
366
|
+
if (!isNonEmptyString(originalValue))
|
|
367
|
+
continue;
|
|
368
|
+
// Find the existing update_props op for this block and add the missing key
|
|
369
|
+
const existingOp = args.plan.ops.find(op => op.op === "update_props" && "blockId" in op && op.blockId === blockId && "pageSlug" in op && op.pageSlug === args.slug);
|
|
370
|
+
if (existingOp && "patch" in existingOp) {
|
|
371
|
+
const patch = existingOp.patch;
|
|
372
|
+
if (!isNonEmptyString(patch[key])) {
|
|
373
|
+
patch[key] = originalValue;
|
|
374
|
+
patched++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (listMatch) {
|
|
379
|
+
const [, blockId, listKey, indexStr, field] = listMatch;
|
|
380
|
+
const idx = Number(indexStr);
|
|
381
|
+
const block = args.currentPage.blocks.find(b => b.id === blockId);
|
|
382
|
+
if (!block)
|
|
383
|
+
continue;
|
|
384
|
+
const listValue = block.props[listKey];
|
|
385
|
+
if (!Array.isArray(listValue) || !listValue[idx])
|
|
386
|
+
continue;
|
|
387
|
+
const originalItem = listValue[idx];
|
|
388
|
+
const originalValue = originalItem[field];
|
|
389
|
+
if (!isNonEmptyString(originalValue))
|
|
390
|
+
continue;
|
|
391
|
+
// Find the existing update_props op and patch the list item
|
|
392
|
+
const existingOp = args.plan.ops.find(op => op.op === "update_props" && "blockId" in op && op.blockId === blockId && "pageSlug" in op && op.pageSlug === args.slug);
|
|
393
|
+
if (existingOp && "patch" in existingOp) {
|
|
394
|
+
const patch = existingOp.patch;
|
|
395
|
+
if (Array.isArray(patch[listKey]) && patch[listKey][idx] && typeof patch[listKey][idx] === "object") {
|
|
396
|
+
const itemPatch = patch[listKey][idx];
|
|
397
|
+
if (!isNonEmptyString(itemPatch[field])) {
|
|
398
|
+
itemPatch[field] = originalValue;
|
|
399
|
+
patched++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (patched > 0) {
|
|
406
|
+
console.log(`[translation-coverage] Auto-patched ${patched} missing fields with original values`);
|
|
407
|
+
return null; // Patched — no gap anymore
|
|
408
|
+
}
|
|
409
|
+
return `Invalid full-page translation coverage. Missing translated fields: ${missingPaths.join(", ")}`;
|
|
410
|
+
}
|
|
411
|
+
export function normalizeVariationTypos(text) {
|
|
412
|
+
return text
|
|
413
|
+
.replace(/\bvariaqtions?\b/gi, "variations")
|
|
414
|
+
.replace(/\bvariatons?\b/gi, "variations")
|
|
415
|
+
.replace(/\bvaritions?\b/gi, "variations")
|
|
416
|
+
.replace(/\bvariants?\b/gi, (m) => m.endsWith("s") ? "variations" : "variation");
|
|
417
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditPlan, Operation, PageDoc } from "@avocadostudio-ai/shared";
|
|
2
|
+
export declare function sentenceCase(text: string): string;
|
|
3
|
+
export declare function futureToPastTense(text: string): string;
|
|
4
|
+
export declare function pastToFutureTense(text: string): string;
|
|
5
|
+
export declare function firstUrlFromText(text: string): string | undefined;
|
|
6
|
+
export declare function looksLikeUserInstruction(value: string): boolean;
|
|
7
|
+
export declare function preferredImageAltText(args: {
|
|
8
|
+
query: string;
|
|
9
|
+
resolvedAlt?: string;
|
|
10
|
+
existingAlt?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function collectMentionedSlugsFromPlan(plan: EditPlan, fallbackSlug?: string): string[];
|
|
13
|
+
export declare function collectMentionedSlugsFromOps(ops: Operation[], fallbackSlug?: string): string[];
|
|
14
|
+
export declare function normalizePlanCopyForUi(plan: EditPlan, currentPage: PageDoc): EditPlan;
|