@bettercms-ai/convert 0.11.0 → 0.12.1

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/dist/index.d.ts CHANGED
@@ -197,6 +197,83 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
197
197
  unlocated: UnlocatedPath[];
198
198
  };
199
199
 
200
+ /**
201
+ * What to DO about a path this run could not bind — an instruction, never a category name.
202
+ *
203
+ * 🔴 A REASON IS A DIAGNOSIS AND AN AGENT CANNOT ACT ON ONE. `IN_EXPRESSION` tells a coding agent
204
+ * that something, somewhere, is an expression; it does not say which file, which line, or what to
205
+ * type. So every pending row leaves with a `fix`: one file, one line, the bytes that are there
206
+ * now, and the one edit that makes the path bindable on the next run. The closed `action` set is
207
+ * the contract the server validates (`src/lib/validation/schemas/conversion-receipt.ts`) and the
208
+ * loop `get_next_steps` reads — an unknown action is a 400, so it is minted in exactly one place.
209
+ */
210
+
211
+ /**
212
+ * The edits an agent can be asked for, and nothing else.
213
+ *
214
+ * `wrap-span` the value has no element of its own — wrap it in `<span data-bcms-field>`.
215
+ * `declare-attr` the element is there — add `data-bcms-field` to it at file:line.
216
+ * `declare-richtext` the container renders markup — bind it with the richtext helper.
217
+ * `bind-expression` the value is computed — replace the expression with the helper's read.
218
+ * `bind-data` the copy lives in a data file or a content entry — bind THAT field.
219
+ * `manual` none of the above is true, and `why` says what a human has to decide.
220
+ */
221
+ type FixAction = "wrap-span" | "declare-attr" | "bind-expression" | "declare-richtext" | "bind-data" | "manual";
222
+ /** One mechanical instruction, at one place in one file. @see FixAction */
223
+ interface PendingFix {
224
+ file: string;
225
+ /** 1-based. */
226
+ line: number;
227
+ /** 1-based, when the literal was found on the line. */
228
+ col?: number;
229
+ /** The bytes at `line`, trimmed — so the agent can see it is looking at the right place. */
230
+ snippet: string;
231
+ action: FixAction;
232
+ /** One sentence, present whenever the action alone does not say enough. */
233
+ why?: string;
234
+ }
235
+ /** Everything about one pending path a fix is derived from. Pure input; no filesystem. */
236
+ interface FixInput {
237
+ /** The copy the repository renders, flattened. Empty when the brief captured none. */
238
+ literal: string;
239
+ kind: string;
240
+ /** The richtext value IS the element's whole inline subtree. @see brief.ts */
241
+ inline?: true;
242
+ /** Files this path's copy was found in, best first. May be empty. */
243
+ candidates: string[];
244
+ /** Source, by path — only the files named in `candidates` need be present. */
245
+ contents: ReadonlyMap<string, string>;
246
+ /** How many ROUTES share this `(scope, path)` in the pending list. @see sharedChrome */
247
+ routes: number;
248
+ }
249
+ /**
250
+ * Where `literal` sits in `content` — tolerant of the two things every template does to copy.
251
+ *
252
+ * A template wraps its copy across lines and indents it, and it puts inline markup in the middle
253
+ * of it (`Read <strong>this</strong> now`). Both are invisible to a reader and fatal to
254
+ * `indexOf`, so the words are matched with whitespace OR a tag between them. Bounded at
255
+ * `MAX_WORDS`, because a paragraph's first dozen words identify it as well as all ninety do.
256
+ */
257
+ declare function findLiteral(content: string, literal: string): {
258
+ index: number;
259
+ length: number;
260
+ } | null;
261
+ /**
262
+ * The fix for one pending row. Always answers: a row without one is a row an agent cannot act on.
263
+ *
264
+ * The FILE is the row's own when it has one (the lane that refused named it), and otherwise the
265
+ * first candidate `locate` found — and when there is neither, the fix still names the route's page
266
+ * file with line 1, because "go and look here" beats "go and look".
267
+ */
268
+ declare function fixFor(row: PendingPath, input: FixInput): PendingFix;
269
+ /**
270
+ * How many ROUTES each `(scope, path)` is pending on — the shared-chrome tell. @see actionFor
271
+ *
272
+ * Counted over the whole pending list rather than per row: one nav label pending on twelve routes
273
+ * is one element, and the fix for every one of those rows is the same sentence about promotion.
274
+ */
275
+ declare function routeCounts(pending: readonly PendingPath[]): Map<string, number>;
276
+
200
277
  /**
201
278
  * What the conversion did, and — the half that matters — what it did not.
202
279
  *
@@ -209,6 +286,7 @@ declare function locate(brief: BriefLike, sources: SourceFile[], options?: Locat
209
286
  * sentence appears in three files" and "this field is bound" are different questions — and a path
210
287
  * is only `rewritten` when EVERY located occurrence of it was.
211
288
  */
289
+
212
290
  type PendingReason = "NO_ORIGINAL" | "NOT_IN_SOURCE" | "DIALECT_UNSUPPORTED" | "IN_EXPRESSION" | "IN_SCRIPT_OR_COMMENT" | "IN_DATA_FILE" | "AMBIGUOUS_LITERAL" | "KIND_MISMATCH" | "SUBSTRING_ONLY"
213
291
  /** An INLINE richtext value whose element holds a component, a `<slot>` or an expression. */
214
292
  | "SUBTREE_NOT_HTML" | "PROP_TARGET_NOT_FOUND" | "PROP_DRILLED_DEEP" | "REPEATER_FIXED_LENGTH"
@@ -231,6 +309,15 @@ interface PendingPath {
231
309
  line?: number;
232
310
  /** What to do about it, in prose — a recipe, not a diagnosis. @see message */
233
311
  hint?: string;
312
+ /**
313
+ * The EDIT that makes this path bindable on the next run — one file, one line, one action.
314
+ *
315
+ * 🔴 THE REASON IS A DIAGNOSIS AND NOBODY CAN ACT ON ONE. Every row carries this, because a
316
+ * conversion ends when nothing is pending and the only way an agent gets there is by being told
317
+ * what to type and where. OPTIONAL in the type so a receipt written by an older version of this
318
+ * package still parses; this package emits one for every row. @see fix.ts
319
+ */
320
+ fix?: PendingFix;
234
321
  }
235
322
  /**
236
323
  * One path that was already DECLARED before this run — and how honestly.
@@ -869,7 +956,7 @@ declare function isKnownHelper(content: string, dialect: Dialect): number | null
869
956
  * renderer's constant occurs in it byte for byte, which is the only thing that keeps the copy a
870
957
  * copy. Change it there first, then paste it here.
871
958
  */
872
- declare const FORM_SCRIPT = "<script>\ndocument.querySelectorAll('form[data-bcms-form]').forEach(function(f){\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var msg=f.querySelector('.bcms-form-msg');\n var data={};\n // A checkboxes group posts one entry per checked box under the SAME name; the old\n // one-at-a-time assignment kept only the last, so \"pick many\" delivered one answer.\n // One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a\n // lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts\n // both an array and a comma string.\n new FormData(f).forEach(function(v,k){\n if(k==='cf-turnstile-response')return;\n if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);\n else data[k]=v;\n });\n var body={data:data};\n var tok=f.querySelector('[name=\"cf-turnstile-response\"]');\n if(tok&&tok.value)body['cf-turnstile-response']=tok.value;\n fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})\n .then(function(r){return r.ok;})\n .then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})\n .catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});\n });\n});\n</script>";
959
+ declare const FORM_SCRIPT = "<script>\nfunction bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}\ndocument.querySelectorAll('form[data-bcms-form]').forEach(function(f){\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var msg=f.querySelector('.bcms-form-msg');\n var data={};\n // A checkboxes group posts one entry per checked box under the SAME name; the old\n // one-at-a-time assignment kept only the last, so \"pick many\" delivered one answer.\n // One checked box still arrives as a scalar \u2014 FormData cannot tell a group from a\n // lone field \u2014 which is exactly why lib/forms/validate.ts's chosenValues() accepts\n // both an array and a comma string.\n new FormData(f).forEach(function(v,k){\n if(k==='cf-turnstile-response')return;\n if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);\n else data[k]=v;\n });\n // Country-picker phone fields post ONE key. The __country select is scaffolding: it is\n // composed into the field's own value here, AFTER collection, so a showIf that points at a\n // phone key still compares the raw digits. Then it is removed from the body.\n f.querySelectorAll('select[name$=\"__country\"]').forEach(function(sel){\n var k=sel.name.slice(0,-9);\n var o=sel.options[sel.selectedIndex];\n data[k]=bcmsPhone(o?(o.getAttribute('data-cc')||''):'',data[k],o?o.getAttribute('data-trunk')==='1':false);\n delete data[k+'__country'];\n });\n var body={data:data};\n var tok=f.querySelector('[name=\"cf-turnstile-response\"]');\n if(tok&&tok.value)body['cf-turnstile-response']=tok.value;\n fetch(f.action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})\n .then(function(r){return r.ok;})\n .then(function(ok){if(msg){msg.hidden=false;msg.textContent=ok?(f.getAttribute('data-bcms-form-success')||'Thanks! Your submission was received.'):'Sorry, something went wrong.';}if(ok)f.reset();})\n .catch(function(){if(msg){msg.hidden=false;msg.textContent='Sorry, something went wrong.';}});\n });\n});\n</script>";
873
960
  /**
874
961
  * Wire every form the brief lists, and say what could not be.
875
962
  *
@@ -1720,4 +1807,4 @@ declare function convertSources(briefIn: Brief, sources: SourceFile[], options?:
1720
1807
  receipt: ConversionReceipt;
1721
1808
  }>;
1722
1809
 
1723
- export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findSites, findSitesTolerant, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
1810
+ export { ASTRO_LANE_PENDING, type AstNode, type AttrBinding, type Brief, type BriefForm, type BriefPage, type BriefPath, COMPONENT_DIR, CONTENT_DIR, type CanvasResult, type CanvasSource, type Carried, type ComponentizeOptions, type ComponentizePlan, type ComponentizePlanComponent, type ComponentizePlanField, type ComponentizePlanPage, type ComponentizePlanSection, type ComponentizeReceipt, type ConversionReceipt, ConvertError, type ConvertErrorCode, type ConvertOptions, DIALECT_RULES, type DataMember, type DataResult, type DeclaredPath, type Dialect, type DynamicBinding, FORM_SCRIPT, type FileDeclaration, type FindSitesResult, type FixAction, FormInvariantError, type FormPendingReason, type FormsReceipt, HELPER_PATH, HELPER_VERSION, type ImageResult, type ImageTarget, type ImportedFrom, type LlmFallback, type LocatedPath, type Node, PARSE_FILE, type ParsedFile, type ParserError, type PathLocator, type PendingFix, type PendingForm, type PendingPath, type PendingReason, type PendingSection, type PlanFile, REGISTRY_MARKER, type ReceiptFile, ReceiptInvariantError, type ReceiptNote, type Rewrite, SECTIONS_LIB, SECTION_MARKER, SKIP_DIRS, SKIP_FILES, SOURCE_EXTENSIONS, SSR_DRAFT_RECIPE, STUB_CONTENT, SectionInvariantError, type SectionPendingReason, type SectionsReceipt, type Site, type SiteWhere, type SourceFile, type Splice, type TargetIdentity, type UnlocatedPath, type ValidationCode, ValidationError, type Xray, type XrayBucket, aliasesFrom, assertForms, assertReceipt, assertSections, assetCandidates, assetImport, attrsFor, bindDataLiterals, bindImages, briefDigest, canvasBridge, carryFor, componentizeSources, convertSources, convertedHere, coverageOf, declaringElements, dialectOf, exportedFunction, extendedConfigs, findLiteral, findSites, findSitesTolerant, fixFor, flat, helperSource, importsIn, isDecorativeGlyph, isDynamicRoute, isKnownHelper, isSourceCandidate, locate, measureUnaddressable, overlapping, pageFilesFor, parseFile, propsAttribute, readBrief, readComponentizePlan, readDeclarations, readExpr, readPlan, relativeImport, resolveSpecifier, rewriteFile, routeCounts, routeOfFile, stripTags, unreadableConfigs, walkAst, wireForms };
package/dist/index.js CHANGED
@@ -5318,6 +5318,138 @@ function coverageOf(receipt) {
5318
5318
  };
5319
5319
  }
5320
5320
 
5321
+ // src/fix.ts
5322
+ var MAX_SNIPPET = 160;
5323
+ var MAX_WORDS = 12;
5324
+ var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5325
+ function findLiteral(content, literal) {
5326
+ const words = literal.split(/\s+/).filter(Boolean).slice(0, MAX_WORDS);
5327
+ if (words.length === 0) return null;
5328
+ const pattern = new RegExp(words.map(escapeRegExp2).join("(?:\\s|<[^>]*>)+"), "u");
5329
+ const match = pattern.exec(content);
5330
+ return match ? { index: match.index, length: match[0].length } : null;
5331
+ }
5332
+ function placeOf(content, index) {
5333
+ const before = content.slice(0, index);
5334
+ const line = before.split("\n").length;
5335
+ const start = before.lastIndexOf("\n") + 1;
5336
+ const end = content.indexOf("\n", index);
5337
+ return {
5338
+ line,
5339
+ col: index - start + 1,
5340
+ snippet: content.slice(start, end < 0 ? content.length : end).trim().slice(0, MAX_SNIPPET)
5341
+ };
5342
+ }
5343
+ function fileHead(content) {
5344
+ return { line: 1, snippet: (content ?? "").split("\n", 1)[0].trim().slice(0, MAX_SNIPPET) };
5345
+ }
5346
+ var isMarkdown = (file) => /\.mdx?$/i.test(file);
5347
+ var isDataModule = (file) => /\.(json|ya?ml|[mc]?[jt]s)$/i.test(file) && !/\.d\.ts$/i.test(file);
5348
+ function inDataRegion(content, index) {
5349
+ if (content.startsWith("---")) {
5350
+ const close = content.indexOf("\n---", 3);
5351
+ if (close >= 0 && index < close) return true;
5352
+ }
5353
+ for (const match of content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) {
5354
+ const start = match.index + match[0].indexOf(">") + 1;
5355
+ if (index >= start && index < start + match[1].length) return true;
5356
+ }
5357
+ return false;
5358
+ }
5359
+ var ELEMENT_ACTIONS = /* @__PURE__ */ new Set([
5360
+ "wrap-span",
5361
+ "declare-attr",
5362
+ "declare-richtext"
5363
+ ]);
5364
+ function actionFor(reason, input, file, inData) {
5365
+ const chosen = reasonAction(reason, input, file, inData);
5366
+ if (inData && ELEMENT_ACTIONS.has(chosen.action)) {
5367
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5368
+ }
5369
+ return chosen;
5370
+ }
5371
+ function reasonAction(reason, input, file, inData) {
5372
+ const richtext = input.kind === "richtext" || input.inline === true;
5373
+ switch (reason) {
5374
+ case "AMBIGUOUS_LITERAL":
5375
+ if (input.routes > 1) {
5376
+ return {
5377
+ action: "manual",
5378
+ why: `${file} renders this value on ${input.routes} routes, so it is shared chrome rather than page copy: promote it to the project Layout (a Layout field is one value for the whole site) and rerun, or give each route its own element.`
5379
+ };
5380
+ }
5381
+ return {
5382
+ action: "declare-attr",
5383
+ why: input.candidates.length > 1 ? `This copy appears in ${input.candidates.join(", ")}; declare the one this route renders.` : "Two paths want this element and nothing says which \u2014 declare the right one by hand."
5384
+ };
5385
+ case "REPEATER_AMBIGUOUS":
5386
+ case "REPEATER_FIXED_LENGTH":
5387
+ return {
5388
+ action: "declare-attr",
5389
+ why: "Declare this row's leaf on the element itself, or rewrite the loop to read the CMS array."
5390
+ };
5391
+ case "IN_EXPRESSION":
5392
+ if (inData) {
5393
+ return { action: "bind-data", why: "The copy is a row in this file's own data \u2014 bind that field." };
5394
+ }
5395
+ return richtext ? { action: "declare-richtext", why: "The container is fed markup \u2014 bind it with the richtext helper." } : { action: "bind-expression", why: "Replace the computed value with the helper's read for this path." };
5396
+ case "DIALECT_UNSUPPORTED":
5397
+ if (isMarkdown(file)) {
5398
+ return {
5399
+ action: "bind-data",
5400
+ why: "This is a content-collection body: bind the element that renders it and make the entry the source."
5401
+ };
5402
+ }
5403
+ return isDataModule(file) ? { action: "bind-data", why: `${file} holds this copy as data \u2014 bind the element that renders it.` } : { action: "manual", why: `No parser here reads ${file}, so the declaration has to be written by hand.` };
5404
+ case "NOT_IN_SOURCE":
5405
+ case "NO_ORIGINAL":
5406
+ return isDataModule(file) || inData ? { action: "bind-data", why: "The template computes this from data \u2014 bind the data field instead of the text." } : {
5407
+ action: "manual",
5408
+ why: "The built page renders this and no source file holds it verbatim; find what formats it."
5409
+ };
5410
+ case "IN_DATA_FILE":
5411
+ case "IMAGE_ASSET_UNRESOLVED":
5412
+ return { action: "bind-data", why: "Bind the data field this value comes from." };
5413
+ case "PROP_TARGET_NOT_FOUND":
5414
+ case "PROP_DRILLED_DEEP":
5415
+ return {
5416
+ action: "wrap-span",
5417
+ why: "The component renders this in a position no attribute reaches \u2014 wrap it in a span of its own."
5418
+ };
5419
+ case "SUBSTRING_ONLY":
5420
+ return { action: "wrap-span", why: "Only part of the element's text is this value \u2014 give it its own span." };
5421
+ case "SUBTREE_NOT_HTML":
5422
+ return { action: "declare-richtext", why: "The value is the element's whole subtree \u2014 bind it as richtext." };
5423
+ default:
5424
+ return { action: "manual", why: `${reason} \u2014 see the receipt's message for what blocked this path.` };
5425
+ }
5426
+ }
5427
+ function fixFor(row, input) {
5428
+ const file = row.file ?? input.candidates[0] ?? "";
5429
+ const content = input.contents.get(file);
5430
+ const at = content ? findLiteral(content, input.literal) : null;
5431
+ const place = at ? placeOf(content, at.index) : { ...fileHead(content), col: void 0 };
5432
+ const { action, why } = actionFor(row.reason, input, file, at !== null && inDataRegion(content, at.index));
5433
+ return {
5434
+ file,
5435
+ line: row.line ?? place.line,
5436
+ ...place.col !== void 0 ? { col: place.col } : {},
5437
+ snippet: place.snippet,
5438
+ action,
5439
+ ...why ? { why } : {}
5440
+ };
5441
+ }
5442
+ function routeCounts(pending) {
5443
+ const routes = /* @__PURE__ */ new Map();
5444
+ for (const row of pending) {
5445
+ const id = `${row.scope}|${row.path}`;
5446
+ const held = routes.get(id) ?? /* @__PURE__ */ new Set();
5447
+ held.add(row.route);
5448
+ routes.set(id, held);
5449
+ }
5450
+ return new Map([...routes].map(([id, set]) => [id, set.size]));
5451
+ }
5452
+
5321
5453
  // src/sites/html.ts
5322
5454
  import { parse as parseHtml } from "parse5";
5323
5455
  var RAW_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "template"]);
@@ -6199,6 +6331,7 @@ import MagicString4 from "magic-string";
6199
6331
  var WIRABLE = ["html", "astro", "jsx"];
6200
6332
  var CONTROLS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
6201
6333
  var FORM_SCRIPT = `<script>
6334
+ function bcmsPhone(cc,raw,trunk){var d=String(raw||'').trim();if(!d)return '';if(d.charAt(0)==='+'){var i=d.replace(/D/g,'');return i?'+'+i:'';}if(!cc)return d;var n=d.replace(/D/g,'');if(trunk&&n.charAt(0)==='0')n=n.slice(1);return n?'+'+cc+n:'';}
6202
6335
  document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
6203
6336
  f.addEventListener('submit',function(e){
6204
6337
  e.preventDefault();
@@ -6214,6 +6347,15 @@ document.querySelectorAll('form[data-bcms-form]').forEach(function(f){
6214
6347
  if(Object.prototype.hasOwnProperty.call(data,k))data[k]=(Array.isArray(data[k])?data[k]:[data[k]]).concat(v);
6215
6348
  else data[k]=v;
6216
6349
  });
6350
+ // Country-picker phone fields post ONE key. The __country select is scaffolding: it is
6351
+ // composed into the field's own value here, AFTER collection, so a showIf that points at a
6352
+ // phone key still compares the raw digits. Then it is removed from the body.
6353
+ f.querySelectorAll('select[name$="__country"]').forEach(function(sel){
6354
+ var k=sel.name.slice(0,-9);
6355
+ var o=sel.options[sel.selectedIndex];
6356
+ data[k]=bcmsPhone(o?(o.getAttribute('data-cc')||''):'',data[k],o?o.getAttribute('data-trunk')==='1':false);
6357
+ delete data[k+'__country'];
6358
+ });
6217
6359
  var body={data:data};
6218
6360
  var tok=f.querySelector('[name="cf-turnstile-response"]');
6219
6361
  if(tok&&tok.value)body['cf-turnstile-response']=tok.value;
@@ -6438,7 +6580,9 @@ var SKIP_DIRS = [
6438
6580
  ".vercel/",
6439
6581
  ".cache/",
6440
6582
  "coverage/",
6441
- "vendor/"
6583
+ "vendor/",
6584
+ "__tests__/",
6585
+ "__mocks__/"
6442
6586
  ];
6443
6587
  var SKIP_FILES = ["package-lock.json", "bun.lock", "bun.lockb", "yarn.lock", "pnpm-lock.yaml"];
6444
6588
  var SOURCE_EXTENSIONS = [
@@ -6476,11 +6620,13 @@ var TEMPLATE_DIRS = /* @__PURE__ */ new Set([
6476
6620
  "_includes",
6477
6621
  "_layouts"
6478
6622
  ]);
6623
+ var isAboutTheSite = (lower) => /\.(test|spec)\.[^./]+$/.test(lower) || /^[^/]*\.config\.[^./]+$/.test(lower);
6479
6624
  function isSourceCandidate(path) {
6480
6625
  const lower = path.toLowerCase();
6481
6626
  if (SKIP_DIRS.some((dir) => lower === dir.slice(0, -1) || lower.startsWith(dir) || lower.includes(`/${dir}`)))
6482
6627
  return false;
6483
6628
  if (SKIP_FILES.includes(lower.split("/").pop() ?? "")) return false;
6629
+ if (isAboutTheSite(lower)) return false;
6484
6630
  if (/(^|\/)[jt]sconfig\.json$/.test(lower)) return true;
6485
6631
  if (!SOURCE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return false;
6486
6632
  if (!PROSE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
@@ -8429,7 +8575,7 @@ var ConvertError = class extends Error {
8429
8575
  code;
8430
8576
  };
8431
8577
  var keyOf = (route, scope, path) => scope === "layout" ? `layout::${path}` : `${route}::page::${path}`;
8432
- var escapeRegExp2 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8578
+ var escapeRegExp3 = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8433
8579
  var identifierFor2 = (slug) => `bcms${slug.replace(/[^A-Za-z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase())}`;
8434
8580
  var UNPLACED = /* @__PURE__ */ new Set(["NOT_IN_SOURCE", "IN_EXPRESSION"]);
8435
8581
  async function drillProp(ctx, fromFile, component, prop, kind, hop) {
@@ -8776,6 +8922,33 @@ async function convertSources(briefIn, sources, options) {
8776
8922
  else outFiles.push({ path, operation: "modify", content });
8777
8923
  }
8778
8924
  pending.push(...canvas.pending);
8925
+ for (const file of outFiles) if (file.operation === "modify") latest.set(file.path, file.content);
8926
+ const pageFilesOrTemplate = (route, files) => {
8927
+ const all = [...files];
8928
+ const exact = pageFilesFor(route, all);
8929
+ if (exact.length > 0) return exact;
8930
+ const wanted = route.split("/").filter(Boolean);
8931
+ return all.filter((file) => {
8932
+ if (!isDynamicRoute(file)) return false;
8933
+ const pattern = routeOfFile(file)?.split("/").filter(Boolean);
8934
+ if (!pattern) return false;
8935
+ const rest = pattern.some((segment) => segment.startsWith("[..."));
8936
+ if (rest ? wanted.length < pattern.length : wanted.length !== pattern.length) return false;
8937
+ return pattern.every((segment, i) => segment.startsWith("[") || segment === wanted[i]);
8938
+ }).sort();
8939
+ };
8940
+ const routes = routeCounts(pending);
8941
+ for (const row of pending) {
8942
+ const target = targets.get(keyOf(row.route, row.scope, row.path));
8943
+ row.fix = fixFor(row, {
8944
+ literal: target?.literal ?? "",
8945
+ kind: row.kind,
8946
+ ...target?.inline ? { inline: true } : {},
8947
+ candidates: locatedByKey.get(keyOf(row.route, row.scope, row.path))?.files ?? pageFilesOrTemplate(row.route, latest.keys()),
8948
+ contents: latest,
8949
+ routes: routes.get(`${row.scope}|${row.path}`) ?? 1
8950
+ });
8951
+ }
8779
8952
  const receipt = assertReceipt({
8780
8953
  briefDigest: digest,
8781
8954
  paths: {
@@ -8988,13 +9161,13 @@ async function plan(file, content, dialect, targets, sites, importAt, outcomes,
8988
9161
  for (const target of sharing) outcomes.set(target.key, missing(literal));
8989
9162
  continue;
8990
9163
  }
8991
- const assigned = sharing.length === 1 ? /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]) : resolvePositionally(sharing, forLiteral);
8992
- if (!assigned) {
8993
- for (const target of sharing) outcomes.set(target.key, "AMBIGUOUS_LITERAL");
8994
- continue;
8995
- }
9164
+ const resolved = sharing.length === 1 ? { assigned: /* @__PURE__ */ new Map([[sharing[0].key, forLiteral]]), ambiguous: /* @__PURE__ */ new Set() } : resolvePositionally(sharing, forLiteral);
8996
9165
  for (const target of sharing) {
8997
- const mine = assigned.get(target.key) ?? [];
9166
+ if (resolved.ambiguous.has(target.key)) {
9167
+ outcomes.set(target.key, "AMBIGUOUS_LITERAL");
9168
+ continue;
9169
+ }
9170
+ const mine = resolved.assigned.get(target.key) ?? [];
8998
9171
  const rewritable = mine.filter((s) => s.where !== "script" && s.where !== "comment" && !s.partial);
8999
9172
  if (rewritable.length === 0) {
9000
9173
  outcomes.set(
@@ -9339,19 +9512,25 @@ function repeatGroups(targets) {
9339
9512
  }
9340
9513
  function resolvePositionally(targets, sites) {
9341
9514
  const assigned = /* @__PURE__ */ new Map();
9342
- const claimed = /* @__PURE__ */ new Set();
9515
+ const ambiguous = /* @__PURE__ */ new Set();
9516
+ const picked = /* @__PURE__ */ new Map();
9517
+ const wanters = /* @__PURE__ */ new Map();
9343
9518
  for (const target of targets) {
9344
9519
  const locator = target.locator;
9345
- if (!locator) return null;
9346
- const candidates = sites.filter(
9347
- (s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)
9348
- );
9349
- const site = locator.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9350
- if (!site || claimed.has(site)) return null;
9351
- claimed.add(site);
9352
- assigned.set(target.key, [site]);
9520
+ const candidates = locator ? sites.filter((s) => s.siblingIndex === locator.siblingIndex && isSubsequence(locator.ancestors, s.ancestors)) : [];
9521
+ const site = locator?.repeat ? candidates[locator.repeat.index] : candidates.length === 1 ? candidates[0] : void 0;
9522
+ if (!site) {
9523
+ ambiguous.add(target.key);
9524
+ continue;
9525
+ }
9526
+ picked.set(target.key, site);
9527
+ wanters.set(site, (wanters.get(site) ?? 0) + 1);
9528
+ }
9529
+ for (const [key, site] of picked) {
9530
+ if ((wanters.get(site) ?? 0) > 1) ambiguous.add(key);
9531
+ else assigned.set(key, [site]);
9353
9532
  }
9354
- return assigned;
9533
+ return { assigned, ambiguous };
9355
9534
  }
9356
9535
  function isSubsequence(wanted, chain) {
9357
9536
  let at = 0;
@@ -9374,7 +9553,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9374
9553
  if (elements.length !== targets.length) return { ok: false, reason: "unverifiable" };
9375
9554
  const locals = helperLocals(reparsed, dialect, file);
9376
9555
  const reads = DIALECT_RULES[dialect].text !== null;
9377
- const boundary = (name) => `(?<![\\w$])${escapeRegExp2(name)}(?![\\w$])`;
9556
+ const boundary = (name) => `(?<![\\w$])${escapeRegExp3(name)}(?![\\w$])`;
9378
9557
  const claimed = /* @__PURE__ */ new Set();
9379
9558
  for (const element of elements) {
9380
9559
  const candidates = targets.filter(
@@ -9382,7 +9561,7 @@ async function tierThree(file, content, dialect, targets, llmFallback) {
9382
9561
  );
9383
9562
  const match = candidates.find((target) => {
9384
9563
  if (!reads) return true;
9385
- const quoted = escapeRegExp2(JSON.stringify(target.path));
9564
+ const quoted = escapeRegExp3(JSON.stringify(target.path));
9386
9565
  if (target.scope === "layout") {
9387
9566
  return locals.bcmsLayout.some(
9388
9567
  (name) => new RegExp(`${boundary(name)}\\s*\\(\\s*${quoted}`).test(element.own)
@@ -9482,8 +9661,10 @@ export {
9482
9661
  dialectOf,
9483
9662
  exportedFunction,
9484
9663
  extendedConfigs,
9664
+ findLiteral,
9485
9665
  findSites,
9486
9666
  findSitesTolerant,
9667
+ fixFor,
9487
9668
  flat,
9488
9669
  helperSource,
9489
9670
  importsIn,
@@ -9505,6 +9686,7 @@ export {
9505
9686
  relativeImport,
9506
9687
  resolveSpecifier,
9507
9688
  rewriteFile,
9689
+ routeCounts,
9508
9690
  routeOfFile,
9509
9691
  stripTags,
9510
9692
  unreadableConfigs,