@bettercms-ai/convert 0.15.0 → 0.16.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
@@ -990,7 +990,11 @@ declare const FORM_SCRIPT = "<script>\nfunction bcmsPhone(cc,raw,trunk){var d=St
990
990
  * `ConversionReceipt` with an empty path arithmetic and the lane's own account under `forms`, so
991
991
  * `submit_conversion_receipt` takes it unchanged — the server's invariant reads `paths` only.
992
992
  */
993
- declare function wireForms(briefIn: Brief, sources: SourceFile[]): Promise<{
993
+ interface WireFormsOptions {
994
+ /** Form id or name → the file its `<form>` is in. The operator's answer to FORM_AMBIGUOUS. */
995
+ pins?: Record<string, string>;
996
+ }
997
+ declare function wireForms(briefIn: Brief, sources: SourceFile[], options_?: WireFormsOptions): Promise<{
994
998
  files: PlanFile[];
995
999
  receipt: ConversionReceipt;
996
1000
  }>;
package/dist/index.js CHANGED
@@ -6493,7 +6493,7 @@ function wire(source, candidate, form) {
6493
6493
  source.appendLeft(el.inner.end - tail.length, `${tail || "\n"}${messageFor(dialect)}`);
6494
6494
  }
6495
6495
  }
6496
- async function wireForms(briefIn, sources) {
6496
+ async function wireForms(briefIn, sources, options_ = {}) {
6497
6497
  const brief = readBrief(briefIn);
6498
6498
  const forms = brief.forms ?? [];
6499
6499
  const candidates = [];
@@ -6531,7 +6531,34 @@ async function wireForms(briefIn, sources) {
6531
6531
  }
6532
6532
  const holds = (c, f) => f.fields.filter((field) => c.controls.has(field.key)).length;
6533
6533
  const covers2 = (c, f) => f.fields.length > 0 && holds(c, f) === f.fields.length;
6534
+ const samePath = (a, b) => a.replace(/\\/g, "/").replace(/^\.\//, "") === b.replace(/\\/g, "/").replace(/^\.\//, "");
6534
6535
  const options = new Map(forms.map((f) => [f.id, candidates.filter((c) => covers2(c, f))]));
6536
+ const pinned = /* @__PURE__ */ new Map();
6537
+ for (const [ref, file] of Object.entries(options_.pins ?? {})) {
6538
+ const form = forms.find((f) => f.id === ref || f.name === ref);
6539
+ if (!form) throw new Error(`--pin ${ref}: the brief lists no form with that id or name.`);
6540
+ if (file.trim() === "") throw new Error(`--pin ${ref}: no file named. Give "<form>=<file>".`);
6541
+ pinned.set(form.id, file);
6542
+ }
6543
+ for (const [id, file] of pinned) {
6544
+ options.set(id, options.get(id).filter((c) => samePath(c.file, file)));
6545
+ for (const other of forms) {
6546
+ const its = pinned.get(other.id);
6547
+ if (other.id === id || its !== void 0 && samePath(its, file)) continue;
6548
+ options.set(other.id, options.get(other.id).filter((c) => !samePath(c.file, file)));
6549
+ }
6550
+ }
6551
+ const beat = /* @__PURE__ */ new Map();
6552
+ for (const c of candidates) {
6553
+ const claimants = forms.filter((f) => options.get(f.id).includes(c));
6554
+ if (claimants.length < 2) continue;
6555
+ const top = Math.max(...claimants.map((f) => holds(c, f)));
6556
+ const winners = claimants.filter((f) => holds(c, f) === top);
6557
+ if (winners.length !== 1) continue;
6558
+ const rivals = claimants.filter((f) => f !== winners[0]);
6559
+ for (const f of rivals) options.set(f.id, options.get(f.id).filter((x) => x !== c));
6560
+ beat.set(c, { winner: winners[0], rivals });
6561
+ }
6535
6562
  const wanted = /* @__PURE__ */ new Map();
6536
6563
  for (const list of options.values()) for (const c of list) wanted.set(c, (wanted.get(c) ?? 0) + 1);
6537
6564
  const matched = [];
@@ -6542,7 +6569,12 @@ async function wireForms(briefIn, sources) {
6542
6569
  for (const form of forms) {
6543
6570
  const list = options.get(form.id) ?? [];
6544
6571
  if (list.length > 1) {
6545
- pend2(form, "FORM_AMBIGUOUS", void 0, `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.`);
6572
+ pend2(
6573
+ form,
6574
+ "FORM_AMBIGUOUS",
6575
+ void 0,
6576
+ `${list.length} forms post every field of "${form.name}": ${list.map((c) => c.file).join(", ")}.${pinned.has(form.id) ? " The pin names that file, and it holds more than one." : ` Name the right one with --pin "${form.name}=<file>".`}`
6577
+ );
6546
6578
  continue;
6547
6579
  }
6548
6580
  if (list.length === 1 && (wanted.get(list[0]) ?? 0) > 1) {
@@ -6551,7 +6583,13 @@ async function wireForms(briefIn, sources) {
6551
6583
  }
6552
6584
  if (list.length === 0) {
6553
6585
  const [best] = candidates.map((c) => ({ c, n: holds(c, form) })).filter((x) => x.n > 0).sort((a, b) => b.n - a.n);
6554
- if (best) {
6586
+ const pin = pinned.get(form.id);
6587
+ if (pin) {
6588
+ pend2(form, "FORM_NOT_IN_SOURCE", pin, `the pinned file has no <form> posting ${form.fields.map((f) => f.key).join(", ")}.`);
6589
+ } else if (best && best.n === form.fields.length) {
6590
+ const owner = forms.find((f) => f !== form && options.get(f.id).includes(best.c));
6591
+ pend2(form, "FORM_NOT_IN_SOURCE", best.c.file, `the only <form> posting ${form.fields.map((f) => f.key).join(", ")} is "${owner?.name ?? "another form"}"'s.`);
6592
+ } else if (best) {
6555
6593
  const missing = form.fields.filter((f) => !best.c.controls.has(f.key)).map((f) => f.key);
6556
6594
  pend2(form, "FIELD_UNMATCHED", best.c.file, `no control posts ${missing.join(", ")} in the <form> that posts its other fields.`);
6557
6595
  } else if (unreadable[0]) {
@@ -6599,6 +6637,14 @@ ${indent}${scriptFor(last.candidate.dialect)}`);
6599
6637
  files.push({ path, operation: "modify", content: source.toString() });
6600
6638
  }
6601
6639
  const notes = onSite.filter(({ form }) => form.status === "draft").map(({ form }) => `publish form ${form.name} in the Forms tab \u2014 a draft form rejects every submission with 403.`);
6640
+ for (const { form, candidate } of matched) {
6641
+ const tie = beat.get(candidate);
6642
+ if (tie?.winner === form) {
6643
+ notes.push(
6644
+ `form ${form.name}: ${candidate.file} posts every field of ${tie.rivals.map((f) => f.name).join(", ")} too, and more of ${form.name}'s, so it went to ${form.name} \u2014 name the file yourself with --pin "<form>=<file>" if that is wrong.`
6645
+ );
6646
+ }
6647
+ }
6602
6648
  for (const { form, candidate } of matched) {
6603
6649
  const keys = form.fields.filter((field) => candidate.foreign.has(candidate.controls.get(field.key))).map((field) => field.key);
6604
6650
  if (keys.length > 0) {