@nodegx/mcp 0.2.2 → 0.2.3

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.
@@ -47137,6 +47137,18 @@ var require_node_catalog_enriched = __commonJS({
47137
47137
  isSignal: false,
47138
47138
  description: "Object path inside the bucket, such as avatars/me.png. Supabase only. Leave blank to use the file's own name"
47139
47139
  },
47140
+ {
47141
+ name: "photoQuality",
47142
+ displayName: "Photo Quality",
47143
+ group: "Shrink Photos",
47144
+ plug: "input",
47145
+ type: {
47146
+ name: "number"
47147
+ },
47148
+ isSignal: false,
47149
+ default: 80,
47150
+ description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk"
47151
+ },
47140
47152
  {
47141
47153
  name: "private",
47142
47154
  displayName: "Private",
@@ -47149,6 +47161,17 @@ var require_node_catalog_enriched = __commonJS({
47149
47161
  default: false,
47150
47162
  description: "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node"
47151
47163
  },
47164
+ {
47165
+ name: "readers",
47166
+ displayName: "Readers",
47167
+ group: "General",
47168
+ plug: "input",
47169
+ type: {
47170
+ name: "string"
47171
+ },
47172
+ isSignal: false,
47173
+ description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only"
47174
+ },
47152
47175
  {
47153
47176
  name: "recordId",
47154
47177
  displayName: "Record ID (PocketBase)",
@@ -47160,6 +47183,18 @@ var require_node_catalog_enriched = __commonJS({
47160
47183
  isSignal: false,
47161
47184
  description: "Existing record to attach the file to. PocketBase only. Leave blank to create a new record as part of the upload"
47162
47185
  },
47186
+ {
47187
+ name: "shrinkTo",
47188
+ displayName: "Shrink To",
47189
+ group: "Shrink Photos",
47190
+ plug: "input",
47191
+ type: {
47192
+ name: "number"
47193
+ },
47194
+ isSignal: false,
47195
+ default: 0,
47196
+ description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked"
47197
+ },
47163
47198
  {
47164
47199
  name: "upload",
47165
47200
  displayName: "Upload",
@@ -47302,6 +47337,15 @@ var require_node_catalog_enriched = __commonJS({
47302
47337
  private: [
47303
47338
  "always: held as handed; read when the next Upload goes"
47304
47339
  ],
47340
+ readers: [
47341
+ "always: held as handed; read when the next Upload goes \u2014 set, those roles may read the file as well as whoever uploaded it, and the file is private"
47342
+ ],
47343
+ shrinkTo: [
47344
+ "always: held as handed; read when the next Upload goes \u2014 above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it"
47345
+ ],
47346
+ photoQuality: [
47347
+ "always: held as handed; read when the next Upload goes, and only when a photo is shrunk \u2014 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel"
47348
+ ],
47305
47349
  bucket: [
47306
47350
  "always: held as handed; read when the next Upload goes \u2014 set, the file goes to this bucket at Path (or under the file's own name)"
47307
47351
  ],
@@ -47318,7 +47362,7 @@ var require_node_catalog_enriched = __commonJS({
47318
47362
  "always: held as handed; read when the next Upload goes, and only with Collection \u2014 a half-filled group is no location at all"
47319
47363
  ],
47320
47364
  upload: [
47321
- "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
47365
+ "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
47322
47366
  ]
47323
47367
  },
47324
47368
  enrichment: {
@@ -105466,7 +105510,7 @@ var require_package = __commonJS({
105466
105510
  "package.json"(exports2, module2) {
105467
105511
  module2.exports = {
105468
105512
  name: "@nodegx/mcp",
105469
- version: "0.2.2",
105513
+ version: "0.2.3",
105470
105514
  description: "Model Context Protocol server exposing NodeGX (OpenNoodl) v2 projects to external AI agents.",
105471
105515
  license: "GPL-3.0-only",
105472
105516
  homepage: "https://nodegx.io",
@@ -156645,6 +156689,18 @@ var node_catalog_default = {
156645
156689
  isSignal: false,
156646
156690
  description: "Object path inside the bucket, such as avatars/me.png. Supabase only. Leave blank to use the file's own name"
156647
156691
  },
156692
+ {
156693
+ name: "photoQuality",
156694
+ displayName: "Photo Quality",
156695
+ group: "Shrink Photos",
156696
+ plug: "input",
156697
+ type: {
156698
+ name: "number"
156699
+ },
156700
+ isSignal: false,
156701
+ default: 80,
156702
+ description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk"
156703
+ },
156648
156704
  {
156649
156705
  name: "private",
156650
156706
  displayName: "Private",
@@ -156657,6 +156713,17 @@ var node_catalog_default = {
156657
156713
  default: false,
156658
156714
  description: "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node"
156659
156715
  },
156716
+ {
156717
+ name: "readers",
156718
+ displayName: "Readers",
156719
+ group: "General",
156720
+ plug: "input",
156721
+ type: {
156722
+ name: "string"
156723
+ },
156724
+ isSignal: false,
156725
+ description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only"
156726
+ },
156660
156727
  {
156661
156728
  name: "recordId",
156662
156729
  displayName: "Record ID (PocketBase)",
@@ -156668,6 +156735,18 @@ var node_catalog_default = {
156668
156735
  isSignal: false,
156669
156736
  description: "Existing record to attach the file to. PocketBase only. Leave blank to create a new record as part of the upload"
156670
156737
  },
156738
+ {
156739
+ name: "shrinkTo",
156740
+ displayName: "Shrink To",
156741
+ group: "Shrink Photos",
156742
+ plug: "input",
156743
+ type: {
156744
+ name: "number"
156745
+ },
156746
+ isSignal: false,
156747
+ default: 0,
156748
+ description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked"
156749
+ },
156671
156750
  {
156672
156751
  name: "upload",
156673
156752
  displayName: "Upload",
@@ -156810,6 +156889,15 @@ var node_catalog_default = {
156810
156889
  private: [
156811
156890
  "always: held as handed; read when the next Upload goes"
156812
156891
  ],
156892
+ readers: [
156893
+ "always: held as handed; read when the next Upload goes \u2014 set, those roles may read the file as well as whoever uploaded it, and the file is private"
156894
+ ],
156895
+ shrinkTo: [
156896
+ "always: held as handed; read when the next Upload goes \u2014 above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it"
156897
+ ],
156898
+ photoQuality: [
156899
+ "always: held as handed; read when the next Upload goes, and only when a photo is shrunk \u2014 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel"
156900
+ ],
156813
156901
  bucket: [
156814
156902
  "always: held as handed; read when the next Upload goes \u2014 set, the file goes to this bucket at Path (or under the file's own name)"
156815
156903
  ],
@@ -156826,7 +156914,7 @@ var node_catalog_default = {
156826
156914
  "always: held as handed; read when the next Upload goes, and only with Collection \u2014 a half-filled group is no location at all"
156827
156915
  ],
156828
156916
  upload: [
156829
- "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
156917
+ "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
156830
156918
  ]
156831
156919
  }
156832
156920
  },
@@ -200422,8 +200510,8 @@ var nodegxDescriptor = {
200422
200510
  // The header works. What it cannot do is make a file private on behalf of
200423
200511
  // nobody, and an author reading "supported" would not expect that.
200424
200512
  "files.private": degraded(
200425
- "A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone.",
200426
- "BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal"
200513
+ "A private upload is locked to the person who made it, so a file uploaded by anything other than a signed-in user \u2014 an admin tool, a cloud function \u2014 ends up private with no owner, and stays readable by everyone. Upload File's Readers names roles that may read it too, such as role:member; a file with Readers is never readable by everyone.",
200514
+ "BCN-007 live, security enforced: uploaded as a signed-in user, the plain URL is 403 for anonymous AND for a second signed-in user. Uploaded with the master key, the identical request with X-NodeGX-File-Private: true serves 200 to anonymous \u2014 files.ts::upload sets owner=null for a non-user principal. P121-TWO001-MEMBERSPHOTO: X-NodeGX-File-Readers adds read-only role entries to the file's ACL; uploaded with the master key and role:crew, anonymous and a user without the role are 403 (files-http.test.ts)"
200427
200515
  ),
200428
200516
  "files.progress": supported("XHR upload path reports progress"),
200429
200517
  "auth.password": supported("nodegx-backend/src/server/users.ts"),
@@ -219762,13 +219850,25 @@ var UploadFile = defineNode({
219762
219850
  // Error always a sentence; v1 read `code || status || 0` and put a message-less envelope on Error.
219763
219851
  // v3 (P109 ISL-030, row C52 ruled "Keep the real name"): the Cloud File keeps the name it was uploaded as (the backend's
219764
219852
  // `filename`, else the sent file's name); v2's dropped `filename` and knew only the stored name.
219765
- version: 3,
219853
+ // v4 (P121 TWO-001 / TWO-008): three inputs, Readers (roles that may read a private file), Shrink To and Photo Quality.
219854
+ version: 4,
219855
+ changes: [
219856
+ {
219857
+ version: 4,
219858
+ says: "Three new inputs: Readers names the roles that may read a private file as well as whoever uploaded it, and Shrink To and Photo Quality make a photo smaller in the browser before it is sent. Left empty, each sends the file as v3 did",
219859
+ row: "P121-TWO001-MEMBERSPHOTO, P121-TWO008-UPLOADSHRINK",
219860
+ migration: { none: "Each new input is empty in an app built on v3, and an empty one changes nothing: the upload is the same request" }
219861
+ }
219862
+ ],
219766
219863
  source: "packages/noodl-viewer-react/src/nodes/std-library/uploadfile.ts; noodl-runtime api/cloudstore.js, api/cloudfile.ts",
219767
219864
  needs: ["backend"],
219768
219865
  worldPool: { backends: [...pool2("uploadFile", FILE_REFS[0]), ...pool2("uploadFile", FILE_REFS[1]).slice(0, 1), ...pool2("uploadFile", FILE_REFS[2]).slice(2, 3)] },
219769
219866
  state: {
219770
219867
  file: void 0,
219771
219868
  private: void 0,
219869
+ readers: void 0,
219870
+ shrinkTo: void 0,
219871
+ photoQuality: void 0,
219772
219872
  bucket: void 0,
219773
219873
  path: void 0,
219774
219874
  collection: void 0,
@@ -219800,6 +219900,35 @@ var UploadFile = defineNode({
219800
219900
  description: "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node",
219801
219901
  examples: [true, false]
219802
219902
  },
219903
+ // P121-TWO001-MEMBERSPHOTO — handed to the store with Private; the backend reads it (files.ts)
219904
+ readers: {
219905
+ type: "string",
219906
+ coerce: "none",
219907
+ displayName: "Readers",
219908
+ group: "General",
219909
+ description: "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only",
219910
+ examples: ["role:member, role:admin", ""]
219911
+ },
219912
+ // P121-TWO008-UPLOADSHRINK — the Shrink Photos group. The canvas step (shrinkphoto.ts) is the browser's: this world
219913
+ // has no decoder, so a photo goes as it was picked, which is what the runtime does when the browser cannot read it.
219914
+ shrinkTo: {
219915
+ type: "number",
219916
+ coerce: "none",
219917
+ default: 0,
219918
+ displayName: "Shrink To",
219919
+ group: "Shrink Photos",
219920
+ description: "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked",
219921
+ examples: [0, 1600]
219922
+ },
219923
+ photoQuality: {
219924
+ type: "number",
219925
+ coerce: "none",
219926
+ default: 80,
219927
+ displayName: "Photo Quality",
219928
+ group: "Shrink Photos",
219929
+ description: "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk",
219930
+ examples: [80, 50]
219931
+ },
219803
219932
  // :111-158
219804
219933
  bucket: {
219805
219934
  ...stringInput("Bucket (Supabase)", "Supabase Storage bucket to upload into. Supabase only; there is no default, and a bucket that does not exist fails with an error that does not say so", ["avatars", ""]),
@@ -219839,6 +219968,21 @@ var UploadFile = defineNode({
219839
219968
  {
219840
219969
  file: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes \u2014 an upload already running keeps the file it was sent with", set: { file: v }, send: [] }),
219841
219970
  private: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes", set: { private: v }, send: [] }),
219971
+ readers: (_s, v) => ({
219972
+ says: "always: held as handed; read when the next Upload goes \u2014 set, those roles may read the file as well as whoever uploaded it, and the file is private",
219973
+ set: { readers: v },
219974
+ send: []
219975
+ }),
219976
+ shrinkTo: (_s, v) => ({
219977
+ says: "always: held as handed; read when the next Upload goes \u2014 above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it",
219978
+ set: { shrinkTo: v },
219979
+ send: []
219980
+ }),
219981
+ photoQuality: (_s, v) => ({
219982
+ says: "always: held as handed; read when the next Upload goes, and only when a photo is shrunk \u2014 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel",
219983
+ set: { photoQuality: v },
219984
+ send: []
219985
+ }),
219842
219986
  bucket: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes \u2014 set, the file goes to this bucket at Path (or under the file's own name)", set: { bucket: v }, send: [] }),
219843
219987
  path: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes, and only with a Bucket", set: { path: v }, send: [] }),
219844
219988
  collection: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes \u2014 with Field set and no Bucket, the file attaches to a record of this collection", set: { collection: v }, send: [] }),
@@ -219846,7 +219990,7 @@ var UploadFile = defineNode({
219846
219990
  field: (_s, v) => ({ says: "always: held as handed; read when the next Upload goes, and only with Collection \u2014 a half-filled group is no location at all", set: { field: v }, send: [] }),
219847
219991
  // :170-172 — a token now, the work at the frame end
219848
219992
  upload: (s) => ({
219849
- says: "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure",
219993
+ says: "always: its own upload at the frame's end \u2014 two presses are two uploads \u2014 with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure",
219850
219994
  set: { presses: s.presses + 1 },
219851
219995
  send: [],
219852
219996
  outcome: "pending"
@@ -219863,7 +220007,7 @@ var UploadFile = defineNode({
219863
220007
  },
219864
220008
  // :172-216
219865
220009
  afterInputs: (s, _i, w) => {
219866
- const r = frame(s, w, !!s.file, "uploadFile", () => ({ file: s.file, private: s.private, target: fileTarget(s) }), UPLOAD_ERROR_CODE, "upload");
220010
+ const r = frame(s, w, !!s.file, "uploadFile", () => ({ file: s.file, private: s.private, readers: s.readers, target: fileTarget(s) }), UPLOAD_ERROR_CODE, "upload");
219867
220011
  if (!("backend" in r) || !r.backend) return r;
219868
220012
  const f = s.file;
219869
220013
  const sent2 = f && typeof f.name === "string" ? f.name : void 0;
@@ -224468,6 +224612,113 @@ function settingsDiagnostics(spec, params, wired = []) {
224468
224612
  return [...shown].map(([key2, message2]) => ({ key: key2, says: spec.diagnostics[key2].says, message: message2 }));
224469
224613
  }
224470
224614
 
224615
+ // ../nodegx-node-spec/src/honesty.ts
224616
+ var SCRIPT_TYPES = Object.freeze(["Expression", "JavaScriptFunction", "Javascript2", "Logic Builder"]);
224617
+ var EXEMPT_TYPES = Object.freeze(["CSS Definition", "Component Children"]);
224618
+ var GRAPH_GRADED_TYPES = Object.freeze([
224619
+ "Component Inputs",
224620
+ "Component Outputs",
224621
+ "Event Receiver",
224622
+ "Event Sender",
224623
+ "For Each Actions",
224624
+ "On App Error",
224625
+ "RunTasks",
224626
+ "net.noodl.ActionDispatcher",
224627
+ "net.noodl.ActionHandler",
224628
+ "net.noodl.ComponentObject",
224629
+ "net.noodl.GlobalStore",
224630
+ "net.noodl.GlobalStore.Set",
224631
+ "net.noodl.GlobalStore.Subscribe",
224632
+ "net.noodl.OptimisticUpdate",
224633
+ "net.noodl.ParentComponentObject",
224634
+ "net.noodl.SetComponentObjectProperties",
224635
+ "net.noodl.SetParentComponentObjectProperties",
224636
+ "net.noodl.StateHistory",
224637
+ "net.noodl.StateHistory.Undo",
224638
+ "net.noodl.StateSnapshot"
224639
+ ]);
224640
+ var SCRIPT_TEXT_PARAMETER = Object.freeze({
224641
+ JavaScriptFunction: "functionScript",
224642
+ Expression: "expression",
224643
+ Javascript2: "code",
224644
+ "Logic Builder": "generatedCode"
224645
+ });
224646
+ function honestyClassifier(options) {
224647
+ const core = new Set(options.coreTypes);
224648
+ const components = new Set(options.components ?? []);
224649
+ const exempt = new Set(EXEMPT_TYPES);
224650
+ const scriptTypes = new Set(SCRIPT_TYPES);
224651
+ const specs2 = options.specs ?? specs;
224652
+ const graph = new Set(options.graphGraded ?? GRAPH_GRADED_TYPES);
224653
+ const known = (t) => {
224654
+ const spec = Object.prototype.hasOwnProperty.call(specs2, t) ? specs2[t] : void 0;
224655
+ const contract = spec && typeof spec.contract === "string" && spec.contract.length > 0;
224656
+ return !!spec && !contract || graph.has(t);
224657
+ };
224658
+ return (type) => {
224659
+ if (typeof type !== "string") return "unspecced";
224660
+ if (type.startsWith("/") || type.startsWith("#") || components.has(type)) return "instance";
224661
+ if (exempt.has(type)) return "exempt";
224662
+ if (scriptTypes.has(type)) return "script";
224663
+ if (known(type)) return "known";
224664
+ if (!core.has(type)) return "script";
224665
+ return "unspecced";
224666
+ };
224667
+ }
224668
+ function scriptLinesOf(node) {
224669
+ const param = SCRIPT_TEXT_PARAMETER[node.type];
224670
+ const text2 = param && node.parameters ? node.parameters[param] : void 0;
224671
+ return typeof text2 === "string" && text2.length > 0 ? text2.split("\n").length : 0;
224672
+ }
224673
+ var percentOf2 = (n, d) => d > 0 ? Math.round(1e3 * n / d) / 10 : null;
224674
+ function tally(nodes, classify) {
224675
+ const h = { known: 0, script: 0, unspecced: 0, instances: 0, exempt: 0, percent: null, scriptLines: 0, percentByLines: null, scriptParts: [], unspeccedTypes: {} };
224676
+ let weight = 0;
224677
+ for (const node of nodes) {
224678
+ const cls = classify(node.type);
224679
+ if (cls === "known") h.known++;
224680
+ else if (cls === "instance") h.instances++;
224681
+ else if (cls === "exempt") h.exempt++;
224682
+ else if (cls === "unspecced") {
224683
+ h.unspecced++;
224684
+ h.unspeccedTypes[node.type] = (h.unspeccedTypes[node.type] ?? 0) + 1;
224685
+ } else {
224686
+ h.script++;
224687
+ const lines = scriptLinesOf(node);
224688
+ h.scriptLines += lines;
224689
+ weight += Math.max(1, lines);
224690
+ h.scriptParts.push({ id: node.id, type: node.type, ...node.label ? { label: node.label } : {}, lines, kit: !SCRIPT_TYPES.includes(node.type) });
224691
+ }
224692
+ }
224693
+ h.percent = percentOf2(h.known, h.known + h.script);
224694
+ h.percentByLines = percentOf2(h.known, h.known + weight);
224695
+ return h;
224696
+ }
224697
+ function componentHonesty(component, options, classify = honestyClassifier(options)) {
224698
+ return { component: component.name, ...tally(component.nodes, classify) };
224699
+ }
224700
+ function projectHonesty(components, options) {
224701
+ const classify = honestyClassifier({ ...options, components: [...options.components ?? [], ...components.map((c) => c.name)] });
224702
+ const rows = components.map((c) => componentHonesty(c, options, classify));
224703
+ const all = tally(
224704
+ components.flatMap((c) => c.nodes),
224705
+ classify
224706
+ );
224707
+ return { ...all, components: rows };
224708
+ }
224709
+ function scriptPartName(part) {
224710
+ return part.label ? part.label : `${part.type} ${part.id}`;
224711
+ }
224712
+ function honestyLine(h, maxNames = 5) {
224713
+ const head = h.percent === null ? `no known or script nodes (${h.unspecced} unspecced)` : `${h.percent}% known (${h.known} known, ${h.script} script${h.unspecced ? `, ${h.unspecced} unspecced` : ""})`;
224714
+ if (!h.scriptParts.length || maxNames <= 0) return head;
224715
+ const parts = [...h.scriptParts].sort((a8, b) => b.lines - a8.lines);
224716
+ const names2 = parts.slice(0, maxNames).map((p) => "`" + scriptPartName(p) + "`");
224717
+ const more = parts.length > maxNames ? ` +${parts.length - maxNames} more` : "";
224718
+ return `${head}; script: ${names2.join(", ")}${more}`;
224719
+ }
224720
+ var HONESTY_SENTENCE = "The honesty number is the share of an app's nodes whose behaviour is specced and graded (known) against the nodes that hold code nobody has checked: Function, Script, Expression and Visual Function nodes, and kit nodes with no spec (script). It is known \xF7 (known + script), counted by node instance; component instances are counted inside their component, and a shipped node still waiting for its spec is reported apart as unspecced. It is not a quality score: a Function written on purpose is still script. It says how much of the app can be explained without reading code, and the script list says which parts cannot.";
224721
+
224471
224722
  // ../nodegx-project-contract/port-context.ts
224472
224723
  function portProjectFromStore(store2) {
224473
224724
  const registry2 = store2.readRegistry().components;
@@ -226067,6 +226318,234 @@ var V2ProjectStore = class {
226067
226318
  }
226068
226319
  };
226069
226320
 
226321
+ // ../nodegx-project-contract/run-on-value-change-migration.ts
226322
+ var RUN_ON_CHANGE_PREFIX2 = "runOnChange-";
226323
+ var RUN_ON_CHANGE_FAMILIES = {
226324
+ // variables/variablebase.ts — `Set` is the control signal on all four types.
226325
+ String: { controlSignal: "saveValue", declared: ["value"] },
226326
+ Number: { controlSignal: "saveValue", declared: ["value"] },
226327
+ Boolean: { controlSignal: "saveValue", declared: ["value"] },
226328
+ Color: { controlSignal: "saveValue", declared: ["value"] },
226329
+ // std-library/condition.ts
226330
+ Condition: { controlSignal: "eval", declared: ["condition"] },
226331
+ // std-library/data/collectionnode2.ts — `array` is a subscription, not a port.
226332
+ Collection2: { controlSignal: "fetch", declared: ["collectionId", "array"] },
226333
+ // std-library/data/modelnode2.ts
226334
+ Model2: { controlSignal: "fetch", declared: ["modelId", "object"] },
226335
+ // std-library/data/dbmodelnode2.ts
226336
+ DbModel2: { controlSignal: "fetch", declared: ["modelId", "record"] },
226337
+ // std-library/data/variablenode2.ts
226338
+ Variable2: { controlSignal: "fetch", declared: ["name", "variable"] },
226339
+ // std-library/data/filtercollectionnode.ts
226340
+ "Filter Collection": {
226341
+ controlSignal: "filter",
226342
+ declared: ["items", "enabled", "array", "filterSettings"]
226343
+ },
226344
+ // std-library/data/filterdbmodelsnode.ts — plus one `fp-<name>` port per visual filter input.
226345
+ FilterDBModels: {
226346
+ controlSignal: "filter",
226347
+ declared: ["items", "enabled", "records", "filterSettings"],
226348
+ discoveredPrefixes: ["fp-"]
226349
+ },
226350
+ // std-library/data/dbcollectionnode2.ts — every governed name is registered at runtime
226351
+ // (`initialize` and `registerInputIfNeeded`), so none of them reaches the catalog; plus one
226352
+ // `qp-<name>` port per visual query input.
226353
+ DbCollection2: {
226354
+ controlSignal: "storageFetch",
226355
+ declared: ["records", "querySettings", "collectionName", "search"],
226356
+ discoveredPrefixes: ["qp-"]
226357
+ },
226358
+ // std-library/componentutils/componentobject.ts
226359
+ "net.noodl.ComponentObject": { controlSignal: "fetch", declared: ["object"] },
226360
+ // noodl-viewer-react/nodes/std-library/componentutils/parentcomponentobject.ts
226361
+ "net.noodl.ParentComponentObject": { controlSignal: "fetch", declared: ["object"] },
226362
+ // std-library/user/user.ts
226363
+ "net.noodl.user.User": { controlSignal: "fetch", declared: ["user"] },
226364
+ // noodl-viewer-react/nodes/controls/text-input.ts — `Set` is the control signal and `Text` the
226365
+ // one value input it silenced.
226366
+ //
226367
+ // ✅ P88 GAM-019 (2026-09-14): this family's checkbox port is now declared. Until then
226368
+ // `createNodeFromReactComponent` built its definition field by field and never copied
226369
+ // `runOnValueChange`, so `defineNode` synthesised nothing and the catalog and the panel carried
226370
+ // no `runOnChange-startValue`, though a saved parameter still reached `registerInputIfNeeded`,
226371
+ // where `defineNode`'s wrapper claimed it and the untick landed. The field is forwarded now, so
226372
+ // the port is in the catalog and the panel, and the validator's parameter check accepts it.
226373
+ "net.noodl.controls.textinput": { controlSignal: "set", declared: ["startValue"] },
226374
+ // std-library/simplejavascript.ts — script inputs are registered as `in-<name>`, so the
226375
+ // checkbox is `runOnChange-in-<name>`. `functionScript` is the definition port and keeps the
226376
+ // old guard deliberately; it is not governed.
226377
+ JavaScriptFunction: { controlSignal: "run", declared: [], discoveredPrefixes: ["in-"] },
226378
+ // std-library/expression.ts — every free identifier in `expression` becomes an input.
226379
+ // `expression` is the definition port and keeps the old guard deliberately.
226380
+ //
226381
+ // 🔴 `staticInputs` must list EVERY declared input: anything else observed on a saved instance
226382
+ // reads as a free variable. P88 GAM-002 (`89e533625`) declared `evaluateAtLoad`, and until it
226383
+ // was listed here a saved `evaluateAtLoad: false` on an Expression whose `run` is wired was
226384
+ // handed `runOnChange-evaluateAtLoad: false`, a checkbox for a port no expression feeds.
226385
+ Expression: {
226386
+ controlSignal: "run",
226387
+ declared: [],
226388
+ discoveredBareIdentifiers: true,
226389
+ staticInputs: ["evaluateAtLoad", "expression", "run"]
226390
+ }
226391
+ };
226392
+ function emptyPlan() {
226393
+ return { writes: [], familyNodes: 0, signalDrivenNodes: 0, preserved: 0 };
226394
+ }
226395
+ function runOnChangePortName(inputName) {
226396
+ return RUN_ON_CHANGE_PREFIX2 + inputName;
226397
+ }
226398
+ function governedInputsFor(node, family, incomingPorts) {
226399
+ const governed = [];
226400
+ const seen = /* @__PURE__ */ new Set();
226401
+ const add = (name) => {
226402
+ if (name.length === 0 || seen.has(name)) return;
226403
+ seen.add(name);
226404
+ governed.push(name);
226405
+ };
226406
+ for (const name of family.declared) add(name);
226407
+ const wantsDiscovery = !!family.discoveredPrefixes || !!family.discoveredBareIdentifiers;
226408
+ if (!wantsDiscovery) return governed;
226409
+ const statics = new Set(family.staticInputs ?? []);
226410
+ const observed = [...incomingPorts];
226411
+ for (const name of Object.keys(node.parameters ?? {})) observed.push(name);
226412
+ for (const port5 of node.ports ?? []) if (typeof port5?.name === "string") observed.push(port5.name);
226413
+ for (const port5 of node.dynamicports ?? []) if (typeof port5?.name === "string") observed.push(port5.name);
226414
+ for (const name of observed) {
226415
+ if (name.indexOf(RUN_ON_CHANGE_PREFIX2) === 0) continue;
226416
+ if (statics.has(name)) continue;
226417
+ if (family.discoveredPrefixes) {
226418
+ if (family.discoveredPrefixes.some((prefix) => name.indexOf(prefix) === 0)) add(name);
226419
+ continue;
226420
+ }
226421
+ add(name);
226422
+ }
226423
+ return governed;
226424
+ }
226425
+ function eachNode2(roots, visit) {
226426
+ for (const node of roots) {
226427
+ if (!node || typeof node !== "object") continue;
226428
+ visit(node);
226429
+ if (Array.isArray(node.children)) eachNode2(node.children, visit);
226430
+ }
226431
+ }
226432
+ function planRunOnValueChangeMigration(project2) {
226433
+ const plan = emptyPlan();
226434
+ if (!project2 || !Array.isArray(project2.components)) return plan;
226435
+ for (const component of project2.components) {
226436
+ const graph = component?.graph;
226437
+ if (!graph || !Array.isArray(graph.roots)) continue;
226438
+ const componentName = typeof component.name === "string" ? component.name : "";
226439
+ const connections = Array.isArray(graph.connections) ? graph.connections : [];
226440
+ const nodeIds = /* @__PURE__ */ new Set();
226441
+ eachNode2(graph.roots, (node) => {
226442
+ if (typeof node.id === "string") nodeIds.add(node.id);
226443
+ });
226444
+ const incomingByNode = /* @__PURE__ */ new Map();
226445
+ for (const connection of connections) {
226446
+ if (!connection || typeof connection.toId !== "string" || typeof connection.toProperty !== "string") continue;
226447
+ if (!nodeIds.has(connection.fromId)) continue;
226448
+ const ports = incomingByNode.get(connection.toId);
226449
+ if (ports) ports.push(connection.toProperty);
226450
+ else incomingByNode.set(connection.toId, [connection.toProperty]);
226451
+ }
226452
+ eachNode2(graph.roots, (node) => {
226453
+ const family = RUN_ON_CHANGE_FAMILIES[node.type];
226454
+ if (!family) return;
226455
+ plan.familyNodes++;
226456
+ const incomingPorts = incomingByNode.get(node.id) ?? [];
226457
+ if (incomingPorts.indexOf(family.controlSignal) === -1) return;
226458
+ plan.signalDrivenNodes++;
226459
+ const parameters = node.parameters ?? {};
226460
+ for (const input of governedInputsFor(node, family, incomingPorts)) {
226461
+ const parameter = runOnChangePortName(input);
226462
+ if (Object.prototype.hasOwnProperty.call(parameters, parameter)) {
226463
+ plan.preserved++;
226464
+ continue;
226465
+ }
226466
+ plan.writes.push({
226467
+ component: componentName,
226468
+ nodeId: node.id,
226469
+ nodeType: node.type,
226470
+ input,
226471
+ parameter
226472
+ });
226473
+ }
226474
+ });
226475
+ }
226476
+ return plan;
226477
+ }
226478
+ var RUN_ON_VALUE_CHANGE_FORMAT_VERSION = "5";
226479
+ function projectPredatesRunOnValueChange(version2) {
226480
+ if (version2 === void 0 || version2 === null || version2 === "") return true;
226481
+ const n = Number(version2);
226482
+ return !Number.isFinite(n) || n < Number(RUN_ON_VALUE_CHANGE_FORMAT_VERSION);
226483
+ }
226484
+ function applyRunOnValueChangeMigration(project2) {
226485
+ return writeGovernedCheckboxes(project2, false);
226486
+ }
226487
+ function writeGovernedCheckboxes(project2, value) {
226488
+ const plan = planRunOnValueChangeMigration(project2);
226489
+ if (plan.writes.length === 0) return plan;
226490
+ const writesByNode = /* @__PURE__ */ new Map();
226491
+ for (const write of plan.writes) {
226492
+ const list5 = writesByNode.get(write.nodeId);
226493
+ if (list5) list5.push(write);
226494
+ else writesByNode.set(write.nodeId, [write]);
226495
+ }
226496
+ for (const component of project2.components ?? []) {
226497
+ const roots = component?.graph?.roots;
226498
+ if (!Array.isArray(roots)) continue;
226499
+ eachNode2(roots, (node) => {
226500
+ const writes = writesByNode.get(node.id);
226501
+ if (!writes) return;
226502
+ const existing = node.parameters ?? {};
226503
+ const rebuilt = {};
226504
+ for (const write of writes) rebuilt[write.parameter] = value;
226505
+ for (const name of Object.keys(existing)) {
226506
+ if (!Object.prototype.hasOwnProperty.call(rebuilt, name)) rebuilt[name] = existing[name];
226507
+ }
226508
+ node.parameters = rebuilt;
226509
+ });
226510
+ }
226511
+ return plan;
226512
+ }
226513
+
226514
+ // ../nodegx-project-contract/format/run-on-change-pins.ts
226515
+ var projectWillBeCleanedUp = (version2) => projectPredatesRunOnValueChange(version2);
226516
+ var incomingOf = (connections, id) => JSON.stringify(
226517
+ (connections ?? []).filter((c) => c.toId === id).map((c) => `${c.fromId}.${c.fromProperty}>${c.toProperty}`).sort()
226518
+ );
226519
+ function pinAuthoredRunOnChange(key2, files, onDisk) {
226520
+ const nodes = files.nodes.nodes ?? [];
226521
+ const plan = planRunOnValueChangeMigration({
226522
+ components: [{ name: key2, graph: { roots: nodes, connections: files.connections.connections } }]
226523
+ });
226524
+ if (plan.writes.length === 0) return [];
226525
+ const before = /* @__PURE__ */ new Map();
226526
+ for (const n of onDisk?.nodes.nodes ?? []) before.set(n.id, n);
226527
+ const authored = (n) => {
226528
+ const old = before.get(n.id);
226529
+ if (!old) return true;
226530
+ if (JSON.stringify(old) !== JSON.stringify(n)) return true;
226531
+ return incomingOf(onDisk?.connections.connections, n.id) !== incomingOf(files.connections.connections, n.id);
226532
+ };
226533
+ const byNode = /* @__PURE__ */ new Map();
226534
+ for (const w of plan.writes) byNode.set(w.nodeId, [...byNode.get(w.nodeId) ?? [], w.parameter]);
226535
+ const written2 = [];
226536
+ for (const node of nodes) {
226537
+ const parameters = byNode.get(node.id);
226538
+ if (!parameters || !authored(node)) continue;
226539
+ const existing = node.parameters ?? {};
226540
+ const rebuilt = {};
226541
+ for (const p of parameters) rebuilt[p] = true;
226542
+ for (const name of Object.keys(existing)) if (!(name in rebuilt)) rebuilt[name] = existing[name];
226543
+ node.parameters = rebuilt;
226544
+ for (const p of parameters) written2.push({ node: node.id, parameter: p });
226545
+ }
226546
+ return written2;
226547
+ }
226548
+
226070
226549
  // ../noodl-editor/src/editor/src/models/AiAssistant/authoring/pageRegistration.ts
226071
226550
  var ROUTER_NODE_TYPES = /* @__PURE__ */ new Set(["Router", "Page Stack"]);
226072
226551
  function isSamePage(a8, b) {
@@ -236237,199 +236716,6 @@ function loadCatalog() {
236237
236716
  var fs6 = __toESM(require("fs"));
236238
236717
  var path5 = __toESM(require("path"));
236239
236718
 
236240
- // ../nodegx-project-contract/run-on-value-change-migration.ts
236241
- var RUN_ON_CHANGE_PREFIX2 = "runOnChange-";
236242
- var RUN_ON_CHANGE_FAMILIES = {
236243
- // variables/variablebase.ts — `Set` is the control signal on all four types.
236244
- String: { controlSignal: "saveValue", declared: ["value"] },
236245
- Number: { controlSignal: "saveValue", declared: ["value"] },
236246
- Boolean: { controlSignal: "saveValue", declared: ["value"] },
236247
- Color: { controlSignal: "saveValue", declared: ["value"] },
236248
- // std-library/condition.ts
236249
- Condition: { controlSignal: "eval", declared: ["condition"] },
236250
- // std-library/data/collectionnode2.ts — `array` is a subscription, not a port.
236251
- Collection2: { controlSignal: "fetch", declared: ["collectionId", "array"] },
236252
- // std-library/data/modelnode2.ts
236253
- Model2: { controlSignal: "fetch", declared: ["modelId", "object"] },
236254
- // std-library/data/dbmodelnode2.ts
236255
- DbModel2: { controlSignal: "fetch", declared: ["modelId", "record"] },
236256
- // std-library/data/variablenode2.ts
236257
- Variable2: { controlSignal: "fetch", declared: ["name", "variable"] },
236258
- // std-library/data/filtercollectionnode.ts
236259
- "Filter Collection": {
236260
- controlSignal: "filter",
236261
- declared: ["items", "enabled", "array", "filterSettings"]
236262
- },
236263
- // std-library/data/filterdbmodelsnode.ts — plus one `fp-<name>` port per visual filter input.
236264
- FilterDBModels: {
236265
- controlSignal: "filter",
236266
- declared: ["items", "enabled", "records", "filterSettings"],
236267
- discoveredPrefixes: ["fp-"]
236268
- },
236269
- // std-library/data/dbcollectionnode2.ts — every governed name is registered at runtime
236270
- // (`initialize` and `registerInputIfNeeded`), so none of them reaches the catalog; plus one
236271
- // `qp-<name>` port per visual query input.
236272
- DbCollection2: {
236273
- controlSignal: "storageFetch",
236274
- declared: ["records", "querySettings", "collectionName", "search"],
236275
- discoveredPrefixes: ["qp-"]
236276
- },
236277
- // std-library/componentutils/componentobject.ts
236278
- "net.noodl.ComponentObject": { controlSignal: "fetch", declared: ["object"] },
236279
- // noodl-viewer-react/nodes/std-library/componentutils/parentcomponentobject.ts
236280
- "net.noodl.ParentComponentObject": { controlSignal: "fetch", declared: ["object"] },
236281
- // std-library/user/user.ts
236282
- "net.noodl.user.User": { controlSignal: "fetch", declared: ["user"] },
236283
- // noodl-viewer-react/nodes/controls/text-input.ts — `Set` is the control signal and `Text` the
236284
- // one value input it silenced.
236285
- //
236286
- // ✅ P88 GAM-019 (2026-09-14): this family's checkbox port is now declared. Until then
236287
- // `createNodeFromReactComponent` built its definition field by field and never copied
236288
- // `runOnValueChange`, so `defineNode` synthesised nothing and the catalog and the panel carried
236289
- // no `runOnChange-startValue`, though a saved parameter still reached `registerInputIfNeeded`,
236290
- // where `defineNode`'s wrapper claimed it and the untick landed. The field is forwarded now, so
236291
- // the port is in the catalog and the panel, and the validator's parameter check accepts it.
236292
- "net.noodl.controls.textinput": { controlSignal: "set", declared: ["startValue"] },
236293
- // std-library/simplejavascript.ts — script inputs are registered as `in-<name>`, so the
236294
- // checkbox is `runOnChange-in-<name>`. `functionScript` is the definition port and keeps the
236295
- // old guard deliberately; it is not governed.
236296
- JavaScriptFunction: { controlSignal: "run", declared: [], discoveredPrefixes: ["in-"] },
236297
- // std-library/expression.ts — every free identifier in `expression` becomes an input.
236298
- // `expression` is the definition port and keeps the old guard deliberately.
236299
- //
236300
- // 🔴 `staticInputs` must list EVERY declared input: anything else observed on a saved instance
236301
- // reads as a free variable. P88 GAM-002 (`89e533625`) declared `evaluateAtLoad`, and until it
236302
- // was listed here a saved `evaluateAtLoad: false` on an Expression whose `run` is wired was
236303
- // handed `runOnChange-evaluateAtLoad: false`, a checkbox for a port no expression feeds.
236304
- Expression: {
236305
- controlSignal: "run",
236306
- declared: [],
236307
- discoveredBareIdentifiers: true,
236308
- staticInputs: ["evaluateAtLoad", "expression", "run"]
236309
- }
236310
- };
236311
- function emptyPlan() {
236312
- return { writes: [], familyNodes: 0, signalDrivenNodes: 0, preserved: 0 };
236313
- }
236314
- function runOnChangePortName(inputName) {
236315
- return RUN_ON_CHANGE_PREFIX2 + inputName;
236316
- }
236317
- function governedInputsFor(node, family, incomingPorts) {
236318
- const governed = [];
236319
- const seen = /* @__PURE__ */ new Set();
236320
- const add = (name) => {
236321
- if (name.length === 0 || seen.has(name)) return;
236322
- seen.add(name);
236323
- governed.push(name);
236324
- };
236325
- for (const name of family.declared) add(name);
236326
- const wantsDiscovery = !!family.discoveredPrefixes || !!family.discoveredBareIdentifiers;
236327
- if (!wantsDiscovery) return governed;
236328
- const statics = new Set(family.staticInputs ?? []);
236329
- const observed = [...incomingPorts];
236330
- for (const name of Object.keys(node.parameters ?? {})) observed.push(name);
236331
- for (const port5 of node.ports ?? []) if (typeof port5?.name === "string") observed.push(port5.name);
236332
- for (const port5 of node.dynamicports ?? []) if (typeof port5?.name === "string") observed.push(port5.name);
236333
- for (const name of observed) {
236334
- if (name.indexOf(RUN_ON_CHANGE_PREFIX2) === 0) continue;
236335
- if (statics.has(name)) continue;
236336
- if (family.discoveredPrefixes) {
236337
- if (family.discoveredPrefixes.some((prefix) => name.indexOf(prefix) === 0)) add(name);
236338
- continue;
236339
- }
236340
- add(name);
236341
- }
236342
- return governed;
236343
- }
236344
- function eachNode2(roots, visit) {
236345
- for (const node of roots) {
236346
- if (!node || typeof node !== "object") continue;
236347
- visit(node);
236348
- if (Array.isArray(node.children)) eachNode2(node.children, visit);
236349
- }
236350
- }
236351
- function planRunOnValueChangeMigration(project2) {
236352
- const plan = emptyPlan();
236353
- if (!project2 || !Array.isArray(project2.components)) return plan;
236354
- for (const component of project2.components) {
236355
- const graph = component?.graph;
236356
- if (!graph || !Array.isArray(graph.roots)) continue;
236357
- const componentName = typeof component.name === "string" ? component.name : "";
236358
- const connections = Array.isArray(graph.connections) ? graph.connections : [];
236359
- const nodeIds = /* @__PURE__ */ new Set();
236360
- eachNode2(graph.roots, (node) => {
236361
- if (typeof node.id === "string") nodeIds.add(node.id);
236362
- });
236363
- const incomingByNode = /* @__PURE__ */ new Map();
236364
- for (const connection of connections) {
236365
- if (!connection || typeof connection.toId !== "string" || typeof connection.toProperty !== "string") continue;
236366
- if (!nodeIds.has(connection.fromId)) continue;
236367
- const ports = incomingByNode.get(connection.toId);
236368
- if (ports) ports.push(connection.toProperty);
236369
- else incomingByNode.set(connection.toId, [connection.toProperty]);
236370
- }
236371
- eachNode2(graph.roots, (node) => {
236372
- const family = RUN_ON_CHANGE_FAMILIES[node.type];
236373
- if (!family) return;
236374
- plan.familyNodes++;
236375
- const incomingPorts = incomingByNode.get(node.id) ?? [];
236376
- if (incomingPorts.indexOf(family.controlSignal) === -1) return;
236377
- plan.signalDrivenNodes++;
236378
- const parameters = node.parameters ?? {};
236379
- for (const input of governedInputsFor(node, family, incomingPorts)) {
236380
- const parameter = runOnChangePortName(input);
236381
- if (Object.prototype.hasOwnProperty.call(parameters, parameter)) {
236382
- plan.preserved++;
236383
- continue;
236384
- }
236385
- plan.writes.push({
236386
- component: componentName,
236387
- nodeId: node.id,
236388
- nodeType: node.type,
236389
- input,
236390
- parameter
236391
- });
236392
- }
236393
- });
236394
- }
236395
- return plan;
236396
- }
236397
- var RUN_ON_VALUE_CHANGE_FORMAT_VERSION = "5";
236398
- function projectPredatesRunOnValueChange(version2) {
236399
- if (version2 === void 0 || version2 === null || version2 === "") return true;
236400
- const n = Number(version2);
236401
- return !Number.isFinite(n) || n < Number(RUN_ON_VALUE_CHANGE_FORMAT_VERSION);
236402
- }
236403
- function applyRunOnValueChangeMigration(project2) {
236404
- return writeGovernedCheckboxes(project2, false);
236405
- }
236406
- function writeGovernedCheckboxes(project2, value) {
236407
- const plan = planRunOnValueChangeMigration(project2);
236408
- if (plan.writes.length === 0) return plan;
236409
- const writesByNode = /* @__PURE__ */ new Map();
236410
- for (const write of plan.writes) {
236411
- const list5 = writesByNode.get(write.nodeId);
236412
- if (list5) list5.push(write);
236413
- else writesByNode.set(write.nodeId, [write]);
236414
- }
236415
- for (const component of project2.components ?? []) {
236416
- const roots = component?.graph?.roots;
236417
- if (!Array.isArray(roots)) continue;
236418
- eachNode2(roots, (node) => {
236419
- const writes = writesByNode.get(node.id);
236420
- if (!writes) return;
236421
- const existing = node.parameters ?? {};
236422
- const rebuilt = {};
236423
- for (const write of writes) rebuilt[write.parameter] = value;
236424
- for (const name of Object.keys(existing)) {
236425
- if (!Object.prototype.hasOwnProperty.call(rebuilt, name)) rebuilt[name] = existing[name];
236426
- }
236427
- node.parameters = rebuilt;
236428
- });
236429
- }
236430
- return plan;
236431
- }
236432
-
236433
236719
  // ../nodegx-export/src/scriptFiles.ts
236434
236720
  function normalizeScriptPath2(path43) {
236435
236721
  if (typeof path43 !== "string") return void 0;
@@ -237114,6 +237400,7 @@ function emitScaffold(ir, shell) {
237114
237400
  function routedPages(ir) {
237115
237401
  const router = ir.project.routers[0];
237116
237402
  if (!router) return [];
237403
+ const prefix = routerPrefix(ir, router);
237117
237404
  const byLegacyPath = /* @__PURE__ */ new Map();
237118
237405
  for (const component of ir.components) byLegacyPath.set(`/${component.path}`, component);
237119
237406
  const usedNames = /* @__PURE__ */ new Set();
@@ -237122,7 +237409,7 @@ function routedPages(ir) {
237122
237409
  const component = byLegacyPath.get(legacyPath);
237123
237410
  if (!component) continue;
237124
237411
  const fileBase = dedupe2(pascalCase(lastSegment(component.path)), usedNames);
237125
- const urlPath = `/${trimUrlPart2(pageUrlPath(component))}`;
237412
+ const urlPath = `/${prefix}${trimUrlPart2(pageUrlPath(component))}`;
237126
237413
  pages.push({
237127
237414
  componentPath: component.path,
237128
237415
  fileBase,
@@ -237135,6 +237422,59 @@ function routedPages(ir) {
237135
237422
  }
237136
237423
  return pages;
237137
237424
  }
237425
+ function routerNodeOf(ir, router) {
237426
+ const component = ir.components.find((c) => c.path === router.componentPath);
237427
+ const node = component?.nodes.find((n) => n.id === router.nodeId);
237428
+ return component && node ? { component, node } : void 0;
237429
+ }
237430
+ function routerPrefix(ir, router) {
237431
+ const value = routerNodeOf(ir, router)?.node.parameters.find((p) => p.name === "urlPath")?.value;
237432
+ if (value?.kind !== "literal" || typeof value.value !== "string") return "";
237433
+ const trimmed = trimUrlPart2(value.value);
237434
+ return trimmed === "" ? "" : `${trimmed}/`;
237435
+ }
237436
+ var ROUTER_OWN_STYLE = [
237437
+ { port: "backgroundColor", label: "Background Color", paints: (t) => t.trim() !== "" && t.trim() !== "transparent" },
237438
+ { port: "clip", label: "Clip Behavior", paints: (t) => t === "scroll" || t === "clip" },
237439
+ { port: "cssClassName", label: "CSS Class", paints: (t) => t.trim() !== "" },
237440
+ // The port's default is a comment, `/* background-color: red; */`: it declares nothing.
237441
+ { port: "styleCss", label: "CSS Style", paints: (t) => t.replace(/\/\*[\s\S]*?\*\//g, "").trim() !== "" }
237442
+ ];
237443
+ var paramText = (value) => value === void 0 ? void 0 : value.kind === "literal" ? String(value.value) : value.kind === "script" || value.kind === "expression" ? value.source : value.kind === "json" ? JSON.stringify(value.value) : `${value.value}${value.unit}`;
237444
+ function routerNotes(ir) {
237445
+ const lines = [];
237446
+ for (const router of ir.project.routers) {
237447
+ const found2 = routerNodeOf(ir, router);
237448
+ if (!found2) continue;
237449
+ const { component, node } = found2;
237450
+ const wired = (port5) => component.connections.some((c) => c.toId === node.id && c.toProperty === port5);
237451
+ const value = (port5) => node.parameters.find((p) => p.name === port5)?.value;
237452
+ const at = `${router.componentPath}: node ${router.nodeId} (Router "${router.name}")`;
237453
+ if (wired("mounted")) {
237454
+ lines.push(`${at}: its Mounted is wired \u2014 the running app shows no page while the value is false; this export always routes`);
237455
+ } else {
237456
+ const mounted = value("mounted");
237457
+ if (mounted?.kind === "literal" && mounted.value === false) {
237458
+ lines.push(`${at}: its Mounted is unticked \u2014 the running app renders no Router, so it shows no page at all; this export routes every page`);
237459
+ }
237460
+ }
237461
+ if (wired("urlPath")) {
237462
+ lines.push(`${at}: its Url Path is wired \u2014 the running app puts the value in front of every page's path; the exported routes carry none`);
237463
+ }
237464
+ const own2 = ROUTER_OWN_STYLE.filter(({ port: port5, paints: paints2 }) => {
237465
+ if (wired(port5)) return true;
237466
+ const text2 = paramText(value(port5));
237467
+ return text2 !== void 0 && paints2(text2);
237468
+ }).map((s) => s.label);
237469
+ if (own2.length > 0) {
237470
+ const names2 = own2.length === 1 ? own2[0] : `${own2.slice(0, -1).join(", ")} and ${own2[own2.length - 1]}`;
237471
+ lines.push(
237472
+ `${at}: its own ${names2} ${own2.length === 1 ? "is" : "are"} not carried \u2014 the export renders the pages with no Router element to put ${own2.length === 1 ? "it" : "them"} on`
237473
+ );
237474
+ }
237475
+ }
237476
+ return lines;
237477
+ }
237138
237478
  function pageUrlPath(component) {
237139
237479
  const pageNode = component.nodes.find((n) => n.type === "Page");
237140
237480
  const urlPath = pageNode?.parameters.find((p) => p.name === "urlPath")?.value;
@@ -237459,8 +237799,10 @@ var RESERVED_WORDS = /* @__PURE__ */ new Set([
237459
237799
  "with",
237460
237800
  "yield"
237461
237801
  ]);
237802
+ var REACT_WITHHELD_PROPS = /* @__PURE__ */ new Set(["key"]);
237462
237803
  var isReservedWord = (name) => RESERVED_WORDS.has(name);
237463
237804
  function propIdentifier(name) {
237805
+ if (REACT_WITHHELD_PROPS.has(name)) return `${name}Prop`;
237464
237806
  if (TS_IDENTIFIER.test(name) && !RESERVED_WORDS.has(name)) return name;
237465
237807
  const words3 = name.split(/[^A-Za-z0-9_$]+/).filter((w) => w.length > 0);
237466
237808
  const joined = words3.map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join("");
@@ -239187,6 +239529,10 @@ var PROPERTY_ORDER = [
239187
239529
  "letter-spacing",
239188
239530
  "text-transform",
239189
239531
  "text-align",
239532
+ // P18-S110-NOWRAP. Text's three white-space arms, together, after the text block they shape.
239533
+ "white-space",
239534
+ "overflow-wrap",
239535
+ "text-overflow",
239190
239536
  "color",
239191
239537
  "opacity",
239192
239538
  "user-select"
@@ -239416,6 +239762,20 @@ function computeNodeStyle(node, role, catalog2, styles) {
239416
239762
  }
239417
239763
  consumed.add("backdropBlur");
239418
239764
  if (role !== "stack" && literal2("clip") === true) decls.push({ prop: "overflow", value: "hidden" });
239765
+ if (node.type === "Text") {
239766
+ const textOverflow = String(literal2("textOverflow") ?? catalogDefault("textOverflow") ?? "wrap");
239767
+ if (textOverflow === "ellipsis" || textOverflow === "clip") {
239768
+ decls.push({ prop: "white-space", value: "nowrap" });
239769
+ decls.push({ prop: "text-overflow", value: textOverflow });
239770
+ if (!decls.some((d) => d.prop === "overflow")) decls.push({ prop: "overflow", value: "hidden" });
239771
+ } else if (sizeMode === "contentSize" || sizeMode === "contentWidth") {
239772
+ decls.push({ prop: "white-space", value: "pre" });
239773
+ } else {
239774
+ decls.push({ prop: "white-space", value: "pre-wrap" });
239775
+ decls.push({ prop: "overflow-wrap", value: "anywhere" });
239776
+ }
239777
+ consumed.add("textOverflow");
239778
+ }
239419
239779
  boxShorthand("padding", params, decls);
239420
239780
  boxShorthand("margin", params, decls);
239421
239781
  for (const side of ["Top", "Right", "Bottom", "Left"]) {
@@ -239569,6 +239929,25 @@ function computeRoleCss(node, role, catalog2, styles) {
239569
239929
  };
239570
239930
  }
239571
239931
  }
239932
+ if (role === "select") {
239933
+ const placeholder = literal2("placeholder");
239934
+ if (typeof placeholder === "string" && placeholder.length > 0) {
239935
+ const raw = params.get("placeholderOpacity");
239936
+ if (raw !== void 0 && raw.kind !== "literal") {
239937
+ result.notes.push(`node ${node.id}'s Placeholder opacity is not a literal; the placeholder is drawn at full opacity`);
239938
+ } else {
239939
+ const authored = Number((raw?.kind === "literal" ? raw.value : void 0) ?? catalogDefault("placeholderOpacity") ?? 0.5);
239940
+ const opacity = Number.isFinite(authored) ? Math.min(1, Math.max(0, authored)) : 1;
239941
+ if (opacity < 1) {
239942
+ const color = cssParam("color") ?? "FieldText";
239943
+ const percent2 = `${Number((opacity * 100).toFixed(4))}%`;
239944
+ const checked = ":has(> option[hidden]:checked)";
239945
+ result.blocks.push({ suffix: checked, decls: [{ prop: "color", value: `color-mix(in srgb, ${color} ${percent2}, transparent)` }] });
239946
+ result.blocks.push({ suffix: `${checked} > option`, decls: [{ prop: "color", value: color }] });
239947
+ }
239948
+ }
239949
+ }
239950
+ }
239572
239951
  if (role === "columns") {
239573
239952
  const minWidth = cssParam("minWidth");
239574
239953
  const pairs = [
@@ -241540,6 +241919,195 @@ ${source}`);
241540
241919
  return scriptFileMarkerDefer(source);
241541
241920
  }
241542
241921
 
241922
+ // ../nodegx-export/src/analyze/querySettings.ts
241923
+ var WIRED_VALUE = "@nodegx/wired-value";
241924
+ var MAX_WIRED_PARAMS = 3;
241925
+ var paramOf = (node, name) => node.parameters.find((p) => p.name === name)?.value;
241926
+ var valueOf = (value) => value === void 0 ? void 0 : value.kind === "literal" || value.kind === "json" ? value.value : void 0;
241927
+ function hasRelatedTo(filter) {
241928
+ if (filter === null || typeof filter !== "object") return false;
241929
+ const record2 = filter;
241930
+ if (Array.isArray(record2.and)) return record2.and.some(hasRelatedTo);
241931
+ if (Array.isArray(record2.or)) return record2.or.some(hasRelatedTo);
241932
+ return "relatedTo" in record2;
241933
+ }
241934
+ function relatedToNeedsALoadedRecord(filter) {
241935
+ if (filter === null || typeof filter !== "object") return false;
241936
+ const record2 = filter;
241937
+ if (Array.isArray(record2.and)) return record2.and.some(relatedToNeedsALoadedRecord);
241938
+ if (Array.isArray(record2.or)) return record2.or.some(relatedToNeedsALoadedRecord);
241939
+ const relatedTo = record2.relatedTo;
241940
+ return relatedTo !== void 0 && (relatedTo.className === void 0 || relatedTo.className === "");
241941
+ }
241942
+ function hasPointsTo(filter) {
241943
+ if (filter === null || typeof filter !== "object") return false;
241944
+ const record2 = filter;
241945
+ if (Array.isArray(record2.and)) return record2.and.some(hasPointsTo);
241946
+ if (Array.isArray(record2.or)) return record2.or.some(hasPointsTo);
241947
+ return Object.values(record2).some((condition) => condition !== null && typeof condition === "object" && "pointsTo" in condition);
241948
+ }
241949
+ function schemaOf(collectionName4, collections) {
241950
+ const collection = collections.find((c) => c.name === collectionName4);
241951
+ if (collection === void 0) return void 0;
241952
+ const properties = {};
241953
+ for (const column of collection.columns) properties[column.name] = { type: column.type };
241954
+ return { collection: collectionName4, properties };
241955
+ }
241956
+ function querySettingsOf(node, component, collections) {
241957
+ const wired = (port5) => component.connections.some((c) => c.toId === node.id && c.toProperty === port5);
241958
+ const collectionName4 = String(valueOf(paramOf(node, "collectionName")) ?? "");
241959
+ for (const port5 of ["storageFilterType", "visualFilter", "visualSort", "storageEnableLimit", "storageLimit", "storageSkip"]) {
241960
+ if (wired(port5)) return { refuse: `its ${port5} input is wired \u2014 which records it asks for must be readable off the graph in this slice` };
241961
+ }
241962
+ if (valueOf(paramOf(node, "storageFilterType")) === "json") {
241963
+ return { refuse: "its Filter is a Javascript filter, a script the runtime runs on every fetch \u2014 the export translates the Visual filter only" };
241964
+ }
241965
+ const search = valueOf(paramOf(node, "search"));
241966
+ if (wired("search") || search !== void 0 && search !== "" && search !== null) {
241967
+ return { refuse: "it searches (the Search input), which is the backend's full-text path \u2014 not exported in this slice" };
241968
+ }
241969
+ const params = {};
241970
+ const visualFilter = valueOf(paramOf(node, "visualFilter"));
241971
+ if (visualFilter !== void 0) {
241972
+ const typedIn = {};
241973
+ for (const p of node.parameters) if (p.name.startsWith("qp-")) typedIn[p.name.slice("qp-".length)] = valueOf(p.value);
241974
+ const schema = schemaOf(collectionName4, collections);
241975
+ const translate = (queryParameters, asked2) => {
241976
+ const read2 = (name) => {
241977
+ asked2.add(name);
241978
+ return queryParameters[name];
241979
+ };
241980
+ let neutral;
241981
+ if (isVisualQueryFormat(visualFilter)) {
241982
+ const parameters = new Proxy(queryParameters, { get: (_target, key2) => typeof key2 === "string" ? read2(key2) : void 0 });
241983
+ neutral = visualQueryToNeutral(visualFilter, parameters);
241984
+ } else {
241985
+ const bare2 = (portName) => portName.startsWith("qp-") ? portName.slice("qp-".length) : portName;
241986
+ neutral = savedFilterToNeutral(visualFilter, (portName) => read2(bare2(portName)), {
241987
+ dropUnresolvedConnected: true
241988
+ });
241989
+ }
241990
+ if (neutral === null || neutral === void 0) return {};
241991
+ if (hasRelatedTo(neutral) && relatedToNeedsALoadedRecord(neutral)) {
241992
+ return { refuse: "its Filter asks a relation question whose class the runtime reads off a loaded record \u2014 not exported in this slice" };
241993
+ }
241994
+ if (hasPointsTo(neutral)) {
241995
+ return { refuse: `its Filter has a "points to" condition, whose target class the export's schema snapshot does not carry` };
241996
+ }
241997
+ let where;
241998
+ try {
241999
+ where = toParseWhere(neutral, { backend: "nodegx", schema });
242000
+ } catch (error2) {
242001
+ if (error2 instanceof FilterTranslationError) {
242002
+ return { refuse: `the runtime cannot translate its Filter either ("${error2.message}"), so it answers Failure and never queries` };
242003
+ }
242004
+ throw error2;
242005
+ }
242006
+ return Object.keys(where).length > 0 ? { where } : {};
242007
+ };
242008
+ const asked = /* @__PURE__ */ new Set();
242009
+ const typedInReading = translate(typedIn, asked);
242010
+ const wiredParams = [...asked].filter((name) => wired(`qp-${name}`));
242011
+ if (wiredParams.length === 0) {
242012
+ if ("refuse" in typedInReading) return typedInReading;
242013
+ if (typedInReading.where !== void 0) params.where = typedInReading.where;
242014
+ } else {
242015
+ const reading = wiredWhereOf(node, component, wiredParams, typedIn, schema, translate);
242016
+ if ("refuse" in reading) return reading;
242017
+ params.wired = reading;
242018
+ }
242019
+ }
242020
+ const visualSort = valueOf(paramOf(node, "visualSort"));
242021
+ if (Array.isArray(visualSort)) {
242022
+ params.sort = visualSort.map((s) => (s?.order === "descending" ? "-" : "") + String(s?.property));
242023
+ }
242024
+ if (valueOf(paramOf(node, "storageEnableLimit"))) {
242025
+ const limit = Number(valueOf(paramOf(node, "storageLimit")) || 10);
242026
+ const skip = Number(valueOf(paramOf(node, "storageSkip")) || 0);
242027
+ params.limit = limit;
242028
+ params.skip = skip;
242029
+ }
242030
+ return { params };
242031
+ }
242032
+ var markerOf2 = (name) => `\0nodegx-qp:${name}\0`;
242033
+ var PLAIN_COMPARISONS = /* @__PURE__ */ new Set(["$eq", "$ne", "$lt", "$gt", "$lte", "$gte"]);
242034
+ function wiredWhereOf(node, component, params, typedIn, schema, translate) {
242035
+ const quoted = params.map((n) => `"${n}"`).join(", ");
242036
+ const wiresInto = (port5) => component.connections.filter((c) => c.toId === node.id && c.toProperty === port5);
242037
+ const fetchWired = wiresInto("storageFetch").length > 0;
242038
+ if (fetchWired && new Set(params.map((name) => valueOf(paramOf(node, `runOnChange-qp-${name}`)) === false)).size > 1) {
242039
+ return { refuse: `its Fetch is wired and the Run On Value Change boxes of ${quoted} disagree \u2014 a value that asks beside one that waits is not exported in this slice` };
242040
+ }
242041
+ if (params.length > MAX_WIRED_PARAMS) {
242042
+ return { refuse: `its Filter reads ${params.length} values from wired inputs; the export builds a \`where\` from at most ${MAX_WIRED_PARAMS}` };
242043
+ }
242044
+ for (const name of params) {
242045
+ if (wiresInto(`qp-${name}`).length > 1) {
242046
+ return { refuse: `two wires feed its "${name}" input \u2014 last-writer-wins is not statically ordered` };
242047
+ }
242048
+ if (paramOf(node, `qp-${name}`) !== void 0) {
242049
+ return { refuse: `its "${name}" input is wired and holds a typed-in value too \u2014 which one the first fetch reads is not exported in this slice` };
242050
+ }
242051
+ if (!fetchWired && valueOf(paramOf(node, `runOnChange-qp-${name}`)) === false) {
242052
+ return {
242053
+ refuse: `its Run On Value Change box for "${name}" is unticked, so a new value waits for a fetch something else starts \u2014 not exported in this slice`
242054
+ };
242055
+ }
242056
+ }
242057
+ const cases = [];
242058
+ for (let mask = 0; mask < 1 << params.length; mask++) {
242059
+ const values = { ...typedIn };
242060
+ params.forEach((name, i) => {
242061
+ values[name] = mask & 1 << i ? markerOf2(name) : void 0;
242062
+ });
242063
+ const reading = translate(values, /* @__PURE__ */ new Set());
242064
+ if ("refuse" in reading) return reading;
242065
+ if (reading.where === void 0) {
242066
+ cases.push(void 0);
242067
+ continue;
242068
+ }
242069
+ const placed2 = placeMarkers(reading.where, params, schema);
242070
+ if ("refuse" in placed2) return placed2;
242071
+ cases.push(placed2.where);
242072
+ }
242073
+ return { params, cases };
242074
+ }
242075
+ function placeMarkers(where, params, schema) {
242076
+ let refusal2;
242077
+ const nameIn = (text2) => params.find((name) => text2.includes(markerOf2(name)));
242078
+ const walk2 = (value, key2, field2) => {
242079
+ if (typeof value === "string") {
242080
+ const name = nameIn(value);
242081
+ if (name === void 0) return value;
242082
+ if (value !== markerOf2(name) || key2 === void 0 || !PLAIN_COMPARISONS.has(key2) || field2 === void 0) {
242083
+ refusal2 ??= `its Filter puts the value of "${name}" somewhere other than a plain comparison (${key2 ?? "the top"}${field2 === void 0 || field2 === key2 ? "" : ` on "${field2}"`}), which the runtime reshapes per value \u2014 not exported in this slice`;
242084
+ return value;
242085
+ }
242086
+ const mark = { [WIRED_VALUE]: name, date: schema?.properties?.[field2]?.type === "Date" };
242087
+ return mark;
242088
+ }
242089
+ if (Array.isArray(value)) return value.map((item) => walk2(item, key2, field2));
242090
+ if (value !== null && typeof value === "object") {
242091
+ const out = {};
242092
+ for (const [k, v] of Object.entries(value)) out[k] = walk2(v, k, k.startsWith("$") ? field2 : k);
242093
+ return out;
242094
+ }
242095
+ return value;
242096
+ };
242097
+ const placed2 = walk2(where, void 0, void 0);
242098
+ return refusal2 === void 0 ? { where: placed2 } : { refuse: refusal2 };
242099
+ }
242100
+ var isWiredValueMark = (value) => value !== null && typeof value === "object" && WIRED_VALUE in value;
242101
+ function wiredWhereCode(where, valueCode) {
242102
+ const codes = [];
242103
+ const json3 = JSON.stringify(where, (_key, value) => {
242104
+ if (!isWiredValueMark(value)) return value;
242105
+ codes.push(valueCode(value[WIRED_VALUE], value.date));
242106
+ return `\0${codes.length - 1}\0`;
242107
+ });
242108
+ return json3.replace(/"\\u0000(\d+)\\u0000"/g, (_m, index) => codes[Number(index)]);
242109
+ }
242110
+
241543
242111
  // ../nodegx-export/src/analyze/appState.ts
241544
242112
  var GLOBAL_STORE = "net.noodl.GlobalStore";
241545
242113
  var GLOBAL_STORE_SET = "net.noodl.GlobalStore.Set";
@@ -241802,7 +242370,7 @@ function collectAppState(ir) {
241802
242370
  if (literalBool(node, "persist") === true || wiredPortsOf(component).has(`${node.id}:persist`)) {
241803
242371
  plan.deferred = plan.deferred ?? "a declarer authors persist \u2014 browser-storage rehydration is not translatable";
241804
242372
  } else if (node.parameters.some((p) => p.name === "storageKey")) {
241805
- plan.notes.push(`declarer \`${node.id}\` sets storageKey without persist \u2014 inert, ignored`);
242373
+ plan.notes.push({ componentPath: component.path, nodeId: node.id, text: "sets storageKey without persist \u2014 inert, ignored" });
241806
242374
  }
241807
242375
  const initial = initialStateOf(node, wiredPortsOf(component));
241808
242376
  if (initial.kind === "object") {
@@ -241812,13 +242380,15 @@ function collectAppState(ir) {
241812
242380
  entry.required = true;
241813
242381
  entry.initial = value;
241814
242382
  } else if (JSON.stringify(entry.initial) !== JSON.stringify(value)) {
241815
- plan.notes.push(
241816
- `two declarers give key "${key2}" different initial values \u2014 first-seen wins (declarer \`${node.id}\` loses)`
241817
- );
242383
+ plan.notes.push({
242384
+ componentPath: component.path,
242385
+ nodeId: node.id,
242386
+ text: `gives key "${key2}" a different initial value from an earlier declarer \u2014 first-seen wins, so this one is ignored`
242387
+ });
241818
242388
  }
241819
242389
  }
241820
242390
  } else if (initial.kind === "bad") {
241821
- plan.notes.push(`declarer \`${node.id}\` has a non-literal or non-object initialState \u2014 contributes nothing`);
242391
+ plan.notes.push({ componentPath: component.path, nodeId: node.id, text: "has a non-literal or non-object initialState \u2014 contributes nothing" });
241822
242392
  }
241823
242393
  }
241824
242394
  if (node.type === GLOBAL_STORE_SET) {
@@ -242373,6 +242943,7 @@ var STACK_COMPLETED_REASON = "its Completed output is consumed \u2014 it fires a
242373
242943
  var CLOUD_FUNCTION_TYPE2 = "CloudFunction2";
242374
242944
  var CLOUD_FUNCTION_OUTPUTS = ["done", "failure", "completed", "error"];
242375
242945
  var RECORD_TYPE = "DbModel2";
242946
+ var VARIABLE_FROZEN = "its Variable changes box is unticked under Run On Value Change \u2014 it keeps the value it read until a Fetch, and the exported variable is read live";
242376
242947
  var RECORD_OUTPUTS = ["done", "failure", "completed", "error", "id"];
242377
242948
  var FILE_PICKER_TYPE = "Open File Picker";
242378
242949
  var UPLOAD_FILE_TYPE = "Upload File";
@@ -243169,6 +243740,11 @@ var USER_VERBS = {
243169
243740
  clearsErrorOnDone: true
243170
243741
  }
243171
243742
  };
243743
+ function isAwaitedAction(value) {
243744
+ if (value === null || typeof value !== "object") return false;
243745
+ const kind = value.kind;
243746
+ return kind === "api-call" || kind === "http-call" || kind === "cloud-call" || kind === "record-fetch" || kind === "file-pick" || kind === "file-upload" || kind === "file-sign" || kind === "crypto-call" && value.async === true;
243747
+ }
243172
243748
  var SESSION_READS = {
243173
243749
  authenticated: "authenticated",
243174
243750
  id: { field: "id", maybeUndefined: true },
@@ -243227,32 +243803,41 @@ function planProject(ir, catalog2) {
243227
243803
  };
243228
243804
  for (const component of ir.components) visit(`/${component.path}`, []);
243229
243805
  const plannedByLegacy = /* @__PURE__ */ new Map();
243230
- for (const legacy of order) {
243806
+ const exclusionsOf = /* @__PURE__ */ new Map();
243807
+ const namesTakenBy = /* @__PURE__ */ new Map();
243808
+ const nameSets = [usedPageNames, usedComponentNames, usedHttpNames];
243809
+ const planOne = (legacy) => {
243810
+ for (const taken of namesTakenBy.get(legacy) ?? []) for (const name of taken.names) taken.set.delete(name);
243811
+ const before = nameSets.map((set2) => new Set(set2));
243231
243812
  const component = ir.components.find((c) => `/${c.path}` === legacy);
243232
- plannedByLegacy.set(
243813
+ const planned = planComponent(
243814
+ component,
243815
+ ir,
243816
+ catalog2,
243817
+ registry2,
243818
+ pageByLegacy,
243819
+ pageFileByPath,
243820
+ usedPageNames,
243821
+ usedComponentNames,
243822
+ kits,
243823
+ usedHttpNames,
243824
+ plannedByLegacy,
243825
+ templateCycle,
243826
+ exclusionsOf.get(legacy)
243827
+ );
243828
+ namesTakenBy.set(
243233
243829
  legacy,
243234
- planComponent(
243235
- component,
243236
- ir,
243237
- catalog2,
243238
- registry2,
243239
- pageByLegacy,
243240
- pageFileByPath,
243241
- usedPageNames,
243242
- usedComponentNames,
243243
- kits,
243244
- usedHttpNames,
243245
- plannedByLegacy,
243246
- templateCycle
243247
- )
243830
+ nameSets.map((set2, i) => ({ set: set2, names: [...set2].filter((name) => !before[i].has(name)) }))
243248
243831
  );
243249
- }
243250
- const plans = ir.components.map((component) => plannedByLegacy.get(`/${component.path}`));
243251
- const byLegacyPath = new Map(plans.map((p) => [p.legacyPath, p]));
243252
- for (const plan of plans) {
243832
+ plannedByLegacy.set(legacy, planned);
243833
+ return planned;
243834
+ };
243835
+ for (const legacy of order) planOne(legacy);
243836
+ const byLegacyNow = (legacy) => plannedByLegacy.get(legacy);
243837
+ const liftInto = (plan) => {
243253
243838
  const liftedVarByKey = /* @__PURE__ */ new Map();
243254
243839
  for (const pending of plan.pendingLifted) {
243255
- const child = byLegacyPath.get(pending.targetLegacy);
243840
+ const child = byLegacyNow(pending.targetLegacy);
243256
243841
  const lifted6 = child?.liftedOutputProps.find((l) => l.port === pending.port);
243257
243842
  if (!lifted6) {
243258
243843
  plan.notes.push(
@@ -243289,7 +243874,56 @@ function planProject(ir, catalog2) {
243289
243874
  };
243290
243875
  }
243291
243876
  plan.pendingLifted = [];
243877
+ };
243878
+ for (const legacy of order) liftInto(plannedByLegacy.get(legacy));
243879
+ for (let round = 0; round < 8; round++) {
243880
+ const again = [];
243881
+ for (const legacy of order) {
243882
+ const planned = plannedByLegacy.get(legacy);
243883
+ const component = ir.components.find((c) => `/${c.path}` === legacy);
243884
+ const found2 = unreachedTriggersOf(planned, component);
243885
+ const known = exclusionsOf.get(legacy) ?? { owners: /* @__PURE__ */ new Map(), mounts: /* @__PURE__ */ new Map(), rows: /* @__PURE__ */ new Map() };
243886
+ let fresh = false;
243887
+ for (const key2 of ["owners", "mounts", "rows"]) {
243888
+ for (const [id, reason2] of found2[key2]) {
243889
+ if (known[key2].has(id)) continue;
243890
+ known[key2].set(id, reason2);
243891
+ fresh = true;
243892
+ }
243893
+ }
243894
+ if (!fresh) continue;
243895
+ exclusionsOf.set(legacy, known);
243896
+ again.push(legacy);
243897
+ }
243898
+ if (again.length === 0) break;
243899
+ for (const legacy of [...again]) {
243900
+ for (const [host, templates] of templatesOf) if (templates.includes(legacy) && !again.includes(host)) again.push(host);
243901
+ }
243902
+ const liftedBefore = new Map(again.map((legacy) => [legacy, JSON.stringify(plannedByLegacy.get(legacy).liftedOutputProps)]));
243903
+ for (const legacy of order) if (again.includes(legacy)) planOne(legacy);
243904
+ for (const [legacy, before] of liftedBefore) {
243905
+ if (JSON.stringify(plannedByLegacy.get(legacy).liftedOutputProps) === before) continue;
243906
+ for (const component of ir.components) {
243907
+ const parent = `/${component.path}`;
243908
+ if (again.includes(parent) || !component.nodes.some((n) => n.type === legacy)) continue;
243909
+ again.push(parent);
243910
+ planOne(parent);
243911
+ }
243912
+ }
243913
+ for (const legacy of again) liftInto(plannedByLegacy.get(legacy));
243914
+ }
243915
+ for (const [legacy, known] of exclusionsOf) {
243916
+ const planned = plannedByLegacy.get(legacy);
243917
+ for (const [fnId, reason2] of known.rows) {
243918
+ const row = planned.stateVars.find((v) => v.origin === "jsfun" && v.originNodeId === fnId);
243919
+ if (row === void 0) continue;
243920
+ const text2 = JSON.stringify({ ...planned, stateVars: void 0, dispositions: void 0, notes: void 0 });
243921
+ if (!text2.includes(`"viaState":"${row.name}"`)) continue;
243922
+ planned.notes.push(`node ${fnId}: a handler or a node that is not drawn still reads its outputs, and reads nothing \u2014 ${reason2.replace(/^it reads /, "")}`);
243923
+ }
243292
243924
  }
243925
+ const plans = ir.components.map((component) => plannedByLegacy.get(`/${component.path}`));
243926
+ const byLegacyPath = new Map(plans.map((p) => [p.legacyPath, p]));
243293
243927
  const stubCollections = [];
243294
243928
  for (const plan of plans) {
243295
243929
  for (const query2 of plan.queries) {
@@ -243309,7 +243943,108 @@ function planProject(ir, catalog2) {
243309
243943
  kitDuplicates
243310
243944
  };
243311
243945
  }
243312
- function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFileByPath, usedPageNames, usedComponentNames, kits, usedHttpNames, plannedByLegacy, templateCycle) {
243946
+ function undrawnReasonOf(plan, component, id) {
243947
+ const nodeById = new Map(component.nodes.map((n) => [n.id, n]));
243948
+ const labelOf = (nodeId) => {
243949
+ const node = nodeById.get(nodeId);
243950
+ return node?.authoredLabel ? `"${node.authoredLabel}" (${nodeId})` : `${node?.type ?? "a node"} ${nodeId}`;
243951
+ };
243952
+ for (let at = id; at !== void 0; at = nodeById.get(at)?.parent) {
243953
+ const whose = at === id ? "it is" : `an element above it, ${labelOf(at)}, is`;
243954
+ const bound = plan.bindings[at]?.["mounted"];
243955
+ if (bound !== void 0) {
243956
+ if (bound.kind === "computed" && (bound.expr.kind === "undefined" || bound.expr.kind === "literal" && !bound.expr.value)) {
243957
+ return `${whose} mounted from a statically false source`;
243958
+ }
243959
+ } else if (nodeById.get(at)?.parameters.find((p) => p.name === "mounted")?.value.kind === "literal" && nodeById.get(at).parameters.find((p) => p.name === "mounted").value.value === false) {
243960
+ const wire = component.connections.find((c) => c.toId === at && c.toProperty === "mounted");
243961
+ if (wire === void 0) return `${whose} authored Mounted false, and nothing mounts it`;
243962
+ const prefix = `wire ${wire.key} dropped: `;
243963
+ const dropped = plan.droppedWires.find((w) => w.key === wire.key)?.reason ?? plan.notes.find((n) => n.startsWith(prefix))?.slice(prefix.length);
243964
+ const from = nodeById.get(wire.fromId);
243965
+ const fromRefused = plan.dispositions[wire.fromId];
243966
+ const because = dropped !== void 0 ? ` (${dropped})` : fromRefused?.kind === "deferred" ? ` (the node it comes from is not exported: ${fromRefused.reason})` : "";
243967
+ return `${whose} authored Mounted false, and the wire into its Mounted from ${from?.authoredLabel ? `"${from.authoredLabel}"` : from?.type ?? wire.fromId}.${wire.fromProperty} is not exported${because}`;
243968
+ }
243969
+ if (at === plan.rootId) break;
243970
+ }
243971
+ return null;
243972
+ }
243973
+ function unreachedTriggersOf(plan, component) {
243974
+ const found2 = { owners: /* @__PURE__ */ new Map(), mounts: /* @__PURE__ */ new Map(), rows: /* @__PURE__ */ new Map() };
243975
+ if (plan.file === null) return found2;
243976
+ const nodeById = new Map(component.nodes.map((n) => [n.id, n]));
243977
+ const labelOf = (nodeId) => {
243978
+ const node = nodeById.get(nodeId);
243979
+ return node?.authoredLabel ? `"${node.authoredLabel}" (${nodeId})` : `${node?.type ?? "a node"} ${nodeId}`;
243980
+ };
243981
+ const whyUndrawn = /* @__PURE__ */ new Map();
243982
+ const owners = /* @__PURE__ */ new Set([...Object.keys(plan.handlers), ...Object.keys(plan.changeHandlers), ...plan.drags.map((d) => d.nodeId)]);
243983
+ for (const owner of owners) {
243984
+ if (plan.roleOf[owner] === void 0) continue;
243985
+ const why = undrawnReasonOf(plan, component, owner);
243986
+ if (why === null) continue;
243987
+ whyUndrawn.set(owner, why);
243988
+ found2.owners.set(owner, `its trigger, ${labelOf(owner)}, is not drawn: ${why}`);
243989
+ }
243990
+ const whyUnmounted = /* @__PURE__ */ new Map();
243991
+ for (const [owner, byPort] of Object.entries(plan.handlers)) {
243992
+ if (byPort["didMount"] === void 0 || whyUndrawn.has(owner)) continue;
243993
+ for (let at = owner; at !== void 0; at = nodeById.get(at)?.parent) {
243994
+ const bound = plan.bindings[at]?.["mounted"];
243995
+ if (bound !== void 0 && !(bound.kind === "computed" && bound.expr.kind === "literal")) {
243996
+ const whose = at === owner ? "it is" : `an element above it, ${labelOf(at)}, is`;
243997
+ const why = `${whose} mounted by a value, so its Did Mount fires when that value says, and a mount effect runs when the component mounts`;
243998
+ whyUnmounted.set(owner, why);
243999
+ found2.mounts.set(owner, `its trigger, the Did Mount of ${labelOf(owner)}, is not run: ${why}`);
244000
+ break;
244001
+ }
244002
+ if (at === plan.rootId) break;
244003
+ }
244004
+ }
244005
+ const writers = /* @__PURE__ */ new Map();
244006
+ const walk2 = (value, owner) => {
244007
+ if (Array.isArray(value)) {
244008
+ for (const item of value) walk2(item, owner);
244009
+ return;
244010
+ }
244011
+ if (value === null || typeof value !== "object") return;
244012
+ const record2 = value;
244013
+ if (record2.kind === "jsfun-run" && typeof record2.materialize === "string") {
244014
+ const list5 = writers.get(record2.materialize) ?? [];
244015
+ list5.push(owner ?? "");
244016
+ writers.set(record2.materialize, list5);
244017
+ }
244018
+ for (const child of Object.values(record2)) walk2(child, owner);
244019
+ };
244020
+ for (const [key2, value] of Object.entries(plan)) {
244021
+ if (key2 === "handlers") {
244022
+ for (const [owner, byPort] of Object.entries(plan.handlers)) {
244023
+ for (const [port5, actions] of Object.entries(byPort)) {
244024
+ walk2(actions, whyUndrawn.has(owner) || port5 === "didMount" && whyUnmounted.has(owner) ? owner : void 0);
244025
+ }
244026
+ }
244027
+ } else if (key2 === "changeHandlers") {
244028
+ for (const [owner, actions] of Object.entries(plan.changeHandlers)) walk2(actions, whyUndrawn.has(owner) ? owner : void 0);
244029
+ } else if (key2 !== "stateVars" && key2 !== "dispositions" && key2 !== "notes") {
244030
+ walk2(value, void 0);
244031
+ }
244032
+ }
244033
+ const planText = JSON.stringify({ ...plan, stateVars: void 0, dispositions: void 0, notes: void 0 });
244034
+ for (const row of plan.stateVars) {
244035
+ if (row.origin !== "jsfun") continue;
244036
+ const fnId = row.originNodeId;
244037
+ const written2 = writers.get(row.name) ?? [];
244038
+ if (written2.some((owner2) => owner2 === "")) continue;
244039
+ if (!planText.includes(`"viaState":"${row.name}"`)) continue;
244040
+ const disposition = plan.dispositions[fnId];
244041
+ const owner = written2[0];
244042
+ const why = owner === void 0 ? disposition?.kind === "deferred" ? disposition.reason : "nothing the export runs sends its Run" : whyUndrawn.has(owner) ? `its Run is sent only from ${labelOf(owner)}, which is not drawn: ${whyUndrawn.get(owner)}` : `its Run is sent only from the Did Mount of ${labelOf(owner)}, which is not run: ${whyUnmounted.get(owner)}`;
244043
+ found2.rows.set(fnId, `it reads ${labelOf(fnId)}, which the export never runs, so its outputs are never written: ${why}`);
244044
+ }
244045
+ return found2;
244046
+ }
244047
+ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFileByPath, usedPageNames, usedComponentNames, kits, usedHttpNames, plannedByLegacy, templateCycle, excluded = { owners: /* @__PURE__ */ new Map(), mounts: /* @__PURE__ */ new Map(), rows: /* @__PURE__ */ new Map() }) {
243313
244048
  const nodeById = new Map(component.nodes.map((n) => [n.id, n]));
243314
244049
  const dispositions = {};
243315
244050
  const notes = [];
@@ -243367,7 +244102,9 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
243367
244102
  recordEffects: [],
243368
244103
  variableSeeds: [],
243369
244104
  objectSeeds: [],
244105
+ fieldMounts: [],
243370
244106
  valueChangedEffects: [],
244107
+ tickedRuns: [],
243371
244108
  styleSheets: [],
243372
244109
  statesMachines: [],
243373
244110
  animations: [],
@@ -243491,6 +244228,10 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
243491
244228
  const renderedStackIds = [];
243492
244229
  const roleOf = (node) => {
243493
244230
  const role = renderRole(node, catalog2, kits);
244231
+ if (role === "repeater" && (literalParam(node, "templateType") === "dynamic" || wiredIn.has(`${node.id}:templateType`))) {
244232
+ deferReasons.set(node.id, "its Template Type is dynamic \u2014 each row's component is chosen by the Template Script at run time, and this slice renders one static template");
244233
+ return "unsupported";
244234
+ }
243494
244235
  if (role === null || role === "unsupported" || role === "instance" || role === "repeater" || role === "slot") return role;
243495
244236
  if (role === "custom") return role;
243496
244237
  if (role === "drag") {
@@ -243801,6 +244542,12 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
243801
244542
  return plan2 !== void 0 && plan2.origin === "object" ? plan2 : void 0;
243802
244543
  };
243803
244544
  const objectNodeGate = (node, store2) => {
244545
+ if (literalParam(node, "runOnChange-modelId") === false) {
244546
+ return "Id is unticked under Run On Value Change \u2014 its properties read nothing until a Fetch, and the exported store is read at once";
244547
+ }
244548
+ if (literalParam(node, "runOnChange-object") === false) {
244549
+ return "Object properties is unticked under Run On Value Change \u2014 its outputs freeze between Fetch pulses, and the exported store is read live";
244550
+ }
243804
244551
  const propInput2 = component.connections.find((c) => c.toId === node.id && c.toProperty.startsWith("prop-"));
243805
244552
  if (propInput2 !== void 0) {
243806
244553
  return `its "${propInput2.toProperty.slice("prop-".length)}" property input is wired \u2014 a write through the Object node itself is not translated in this slice; a Set Object Properties naming "${store2.name}" is`;
@@ -244336,6 +245083,8 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244336
245083
  const logicNodeIds = [];
244337
245084
  const subscriberIds = [];
244338
245085
  let anyDelivery = false;
245086
+ let anyLiteral = false;
245087
+ const wireFed = [];
244339
245088
  jsResolving.add(node.id);
244340
245089
  try {
244341
245090
  for (const name of inputNames) {
@@ -244345,6 +245094,7 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244345
245094
  return { defer: `two wires feed input "${name}" \u2014 last-writer-wins is not statically ordered` };
244346
245095
  }
244347
245096
  const runChangeParam = kind === "function" ? `runOnChange-in-${name}` : `runOnChange-${name}`;
245097
+ if (literalParam(node, port5) !== void 0) anyLiteral = true;
244348
245098
  if (wires.length === 1) {
244349
245099
  const wire = wires[0];
244350
245100
  if (mode === "reactive" && literalParam(node, runChangeParam) === false) {
@@ -244364,6 +245114,7 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244364
245114
  return { defer: `input "${name}" is fed a logic truth value \u2014 only truthiness sinks take one in this slice` };
244365
245115
  }
244366
245116
  anyDelivery = true;
245117
+ wireFed.push(name);
244367
245118
  const tsType = exprTsType(expr);
244368
245119
  inputs.push({
244369
245120
  name,
@@ -244386,8 +245137,12 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244386
245137
  } finally {
244387
245138
  jsResolving.delete(node.id);
244388
245139
  }
244389
- if (kind === "expression" && mode === "reactive" && exprIds.ports.length > 0 && !anyDelivery) {
244390
- return { defer: "none of its inputs is ever delivered \u2014 the expression never evaluates (its outputs abstain null)" };
245140
+ const evaluateAtLoad = literalParam(node, "evaluateAtLoad") !== false;
245141
+ const loadGate = kind === "expression" && mode === "reactive" && exprIds.ports.length > 0 && !anyLiteral ? { unset: wireFed, atLoad: evaluateAtLoad ? "evaluate" : "wait" } : void 0;
245142
+ if (loadGate?.atLoad === "wait" && !anyDelivery) {
245143
+ return {
245144
+ defer: "Evaluate At Load is unticked and none of its inputs is ever delivered \u2014 the expression never evaluates (its outputs abstain null)"
245145
+ };
244391
245146
  }
244392
245147
  const outputs2 = [];
244393
245148
  const signals = [];
@@ -244437,6 +245192,7 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244437
245192
  signals,
244438
245193
  mathAliases: exprIds.mathAliases,
244439
245194
  mode,
245195
+ ...loadGate !== void 0 ? { loadGate } : {},
244440
245196
  ...scriptUses3.length > 0 ? { scripts: scriptUses3 } : {},
244441
245197
  ...kind === "visual" ? {
244442
245198
  variables: [
@@ -244472,8 +245228,13 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244472
245228
  return null;
244473
245229
  }
244474
245230
  if (jsResolving.size > 0) {
244475
- ctx.defer = "it is fed by another Function/Expression node \u2014 JS-node chains are not translated in this slice";
244476
- return null;
245231
+ const runKey = `${fromNode.id}:run`;
245232
+ const materialized = kind === "function" && wiredPorts.has(runKey) && !jsResolving.has(fromNode.id) && jsMaterializedVars.has(fromNode.id);
245233
+ if (!materialized) {
245234
+ const compiled2 = kind === "function" ? compiledSinks.get(runKey) : void 0;
245235
+ ctx.defer = compiled2 !== void 0 && "defer" in compiled2 ? `the Function that feeds it is not exported: ${compiled2.defer}` : "it is fed by another Function/Expression node \u2014 JS-node chains are not translated in this slice";
245236
+ return null;
245237
+ }
244477
245238
  }
244478
245239
  const record2 = jsFunDefOf(fromNode);
244479
245240
  if ("defer" in record2) {
@@ -244502,8 +245263,9 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244502
245263
  switch (output) {
244503
245264
  case "result":
244504
245265
  return base;
244505
- // The abstain-null pre-first-evaluation state is unreachable in the emitted world —
244506
- // every vocabulary source has a boot value (§4, noted not modeled).
245266
+ // The abstain-null pre-first-evaluation state: Result is `null` there (a `loadGate`,
245267
+ // P18-S110-EVALATLOAD), but the four folds below still compute from it where the runtime's
245268
+ // getters abstain (§4, noted not modeled) — `isTrue` reads false, not "nothing yet".
244507
245269
  case "isTrue":
244508
245270
  return truthyExpr(base);
244509
245271
  case "isFalse":
@@ -244546,6 +245308,19 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244546
245308
  return stateVar;
244547
245309
  };
244548
245310
  const verbErrorVars = /* @__PURE__ */ new Map();
245311
+ const verbChainScope = /* @__PURE__ */ new Map();
245312
+ const chainSenders = [];
245313
+ const valueWiresInto = (node) => component.connections.filter((c) => c.toId === node.id && c.toProperty === "value" && c.kind !== "signal");
245314
+ const lastValueWireOf = (node) => {
245315
+ const wires = valueWiresInto(node);
245316
+ if (wires.length < 2) return wires[0] ?? component.connections.find((c) => c.toId === node.id && c.toProperty === "value");
245317
+ for (const sender of [...chainSenders].reverse()) {
245318
+ const own2 = wires.filter((c) => c.fromId === sender);
245319
+ if (own2.length === 1) return own2[0];
245320
+ if (own2.length > 1) return void 0;
245321
+ }
245322
+ return void 0;
245323
+ };
244549
245324
  const attachedRecordVerbs = /* @__PURE__ */ new Set();
244550
245325
  const sessionReadGate = (node) => {
244551
245326
  if (wiredPorts.has(`${node.id}:fetch`)) {
@@ -244763,7 +245538,8 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244763
245538
  null,
244764
245539
  node.id,
244765
245540
  "record-row-error",
244766
- `The Error output of ${node.authoredLabel ? `"${node.authoredLabel}"` : "the Record"} \u2014 why the last read failed, and never cleared by a later success (dbmodelnode2.ts).`
245541
+ // P18-S113-IDBINDREAD. With Fetch unwired the node is the Id bind, which reads nothing and so reports nothing.
245542
+ wiredPorts.has(`${node.id}:fetch`) ? `The Error output of ${node.authoredLabel ? `"${node.authoredLabel}"` : "the Record"} \u2014 why the last read failed, and never cleared by a later success (dbmodelnode2.ts).` : `The Error output of ${node.authoredLabel ? `"${node.authoredLabel}"` : "the Record"} \u2014 its Fetch is unwired and only a Fetch reports one, so it stays empty (dbmodelnode2.ts).`
244767
245543
  );
244768
245544
  recordErrorVars.set(node.id, stateVar);
244769
245545
  }
@@ -244781,16 +245557,27 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
244781
245557
  null,
244782
245558
  node.id,
244783
245559
  "record-row",
244784
- `The record the last ${names2.fnName} read \u2014 undefined until the first read, as the node's property outputs read before one has been made.`
245560
+ wiredPorts.has(`${node.id}:fetch`) ? `The record the last ${names2.fnName} read \u2014 undefined until the first read, as the node's property outputs read before one has been made.` : `The app's copy of the record ${node.authoredLabel ? `"${node.authoredLabel}"` : "the Record"}'s Id names \u2014 undefined until the app holds it, as the node's property outputs read before anything has filled the record (dbmodelnode2.ts setModelID).`
244785
245561
  );
244786
245562
  recordAnswerVars.set(node.id, stateVar);
244787
245563
  }
244788
245564
  return stateVar;
244789
245565
  };
244790
245566
  const attachedRecordNodes = /* @__PURE__ */ new Set();
245567
+ const recordBindRefusal = (node) => {
245568
+ if (literalParam(node, "runOnChange-modelId") === false) {
245569
+ return "its Id is unticked under Run On Value Change and its Fetch is unwired \u2014 the runtime never reads the row";
245570
+ }
245571
+ const pulse = component.connections.find((c) => c.fromId === node.id && (c.fromProperty === "done" || c.fromProperty === "failure"));
245572
+ if (pulse !== void 0) {
245573
+ return `its ${pulse.fromProperty === "done" ? "Done" : "Failure"} output is consumed and its Fetch is unwired \u2014 both fire only after a Fetch, and an Id only binds the record it names (dbmodelnode2.ts setModelID)`;
245574
+ }
245575
+ return void 0;
245576
+ };
244791
245577
  const recordWillFire = (node) => {
244792
245578
  if (wiredPorts.has(`${node.id}:fetch`)) return false;
244793
245579
  if (!plan.file) return false;
245580
+ if (recordBindRefusal(node) !== void 0) return false;
244794
245581
  const compiled2 = compiledOf(node, "fetch");
244795
245582
  return !("defer" in compiled2) && actionsValidIn([compiled2.action], { kind: "render" });
244796
245583
  };
@@ -245077,9 +245864,53 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
245077
245864
  const usedQueryStateNames = /* @__PURE__ */ new Set();
245078
245865
  const queryReaders = /* @__PURE__ */ new Set();
245079
245866
  const staticRowsReadIds = /* @__PURE__ */ new Set();
245867
+ const querySettingsCache = /* @__PURE__ */ new Map();
245868
+ const queryWirings = /* @__PURE__ */ new Map();
245869
+ const querySettingsFor = (node) => {
245870
+ let settings = querySettingsCache.get(node.id);
245871
+ if (settings === void 0) {
245872
+ querySettingsCache.set(node.id, { refuse: "its Filter reads a value that depends on the query itself" });
245873
+ settings = querySettingsOf(node, component, ir.project.collections);
245874
+ if ("params" in settings && settings.params.wired !== void 0) settings = wireQueryValues(node, settings.params.wired.params) ?? settings;
245875
+ querySettingsCache.set(node.id, settings);
245876
+ }
245877
+ return settings;
245878
+ };
245879
+ const wireQueryValues = (node, names2) => {
245880
+ const wiring = { values: [], consumes: [], collapses: [], subscribes: [] };
245881
+ for (const name of names2) {
245882
+ const wire = component.connections.find((c) => c.toId === node.id && c.toProperty === `qp-${name}`);
245883
+ if (wire === void 0) return { refuse: `its "${name}" input has no wire` };
245884
+ const ctx = newCtx();
245885
+ const expr = resolveExpr(nodeById.get(wire.fromId), wire.fromProperty, ctx);
245886
+ if (expr === null) return { refuse: `its Filter reads "${name}" from a wire the export cannot read: ${ctx.defer ?? "no statically known source"}` };
245887
+ if (!exprValidIn(expr, { kind: "render" })) return { refuse: `its Filter reads "${name}" from a value that only exists inside a handler` };
245888
+ wiring.values.push({ name, expr });
245889
+ wiring.consumes.push(wire.key, ...ctx.consumes);
245890
+ wiring.collapses.push(...ctx.logicNodeIds);
245891
+ wiring.subscribes.push(...ctx.subscriberIds);
245892
+ }
245893
+ queryWirings.set(node.id, wiring);
245894
+ return void 0;
245895
+ };
245896
+ const queryLands = (node) => Object.values(plan.repeaters).some((r) => r.itemsQueryId === node.id) || queryReaders.has(node.id) && component.connections.some((c) => c.fromId === node.id && c.fromProperty === "items" && consumed.has(c.key));
245897
+ const pageMountFetch = (wire) => wire.toProperty === "storageFetch" && wire.fromProperty === "didMount" && nodeById.get(wire.fromId)?.type === "Page" && nodeById.get(wire.toId)?.type === "DbCollection2";
245898
+ const fetchWiredOf = (node) => {
245899
+ const fetches2 = component.connections.filter((c) => c.toId === node.id && c.toProperty === "storageFetch");
245900
+ if (fetches2.length === 0) return void 0;
245901
+ const ticked3 = (input) => literalParam(node, `runOnChange-${input}`) !== false;
245902
+ const NOT_QUERY_SETTINGS = /* @__PURE__ */ new Set(["collectionName", "search", "realtime"]);
245903
+ const setting4 = node.parameters.some((p) => !NOT_QUERY_SETTINGS.has(p.name) && !p.name.startsWith("qp-") && !p.name.startsWith("runOnChange-"));
245904
+ const wired = queryWirings.get(node.id)?.values ?? [];
245905
+ return {
245906
+ mount: ticked3("collectionName") || setting4 && ticked3("querySettings") || fetches2.some(pageMountFetch),
245907
+ onValues: wired.length > 0 && wired.every((v) => ticked3(`qp-${v.name}`))
245908
+ };
245909
+ };
245080
245910
  const queryPlanOf = (node) => {
245081
245911
  const existing = plan.queries.find((q) => q.nodeId === node.id);
245082
245912
  if (existing !== void 0) return existing;
245913
+ const settings = querySettingsFor(node);
245083
245914
  const collectionName4 = String(literalParam(node, "collectionName") ?? "Record");
245084
245915
  const { typeName: typeName3, plural: plural3, moduleBase, fetchName } = collectionModuleNames(collectionName4);
245085
245916
  const stateName = dedupe4(plural3, usedQueryStateNames);
@@ -245091,7 +245922,10 @@ function planComponent(component, ir, catalog2, registry2, pageByLegacy, pageFil
245091
245922
  itemName: typeName3.charAt(0).toLowerCase() + typeName3.slice(1),
245092
245923
  fetchName,
245093
245924
  typeName: typeName3,
245094
- moduleBase
245925
+ moduleBase,
245926
+ ..."params" in settings && Object.keys(settings.params).length > 0 ? { params: settings.params } : {},
245927
+ ...queryWirings.has(node.id) ? { wiredValues: queryWirings.get(node.id).values, wiredClient: ir.project.cloudservices !== void 0 } : {},
245928
+ ...fetchWiredOf(node) !== void 0 ? { fetchWired: fetchWiredOf(node) } : {}
245095
245929
  };
245096
245930
  plan.queries.push(query2);
245097
245931
  return query2;
@@ -245774,6 +246608,12 @@ ${body}
245774
246608
  ctx.defer = `its ${wiredSetting.slice(node.id.length + 1)} setting is wired \u2014 the filter is not statically known`;
245775
246609
  return null;
245776
246610
  }
246611
+ for (const box of ["items", "array"]) {
246612
+ if (literalParam(node, `runOnChange-${box}`) === false) {
246613
+ ctx.defer = `its Run On Value Change box for "${box === "items" ? "Items" : "Array changes"}" is unticked \u2014 it would re-filter only on its Filter signal, which this slice does not translate`;
246614
+ return null;
246615
+ }
246616
+ }
245777
246617
  if (literalParam(node, "enabled") === false) {
245778
246618
  ctx.logicNodeIds.push(node.id);
245779
246619
  return source;
@@ -246018,6 +246858,11 @@ ${body}
246018
246858
  ctx.defer = "the Query Records it reads names no Class statically";
246019
246859
  return null;
246020
246860
  }
246861
+ const settings = querySettingsFor(fromNode);
246862
+ if ("refuse" in settings) {
246863
+ ctx.defer = `the Query Records it reads is not exported: ${settings.refuse}`;
246864
+ return null;
246865
+ }
246021
246866
  const query2 = queryPlanOf(fromNode);
246022
246867
  queryReaders.add(fromNode.id);
246023
246868
  return { kind: "query-get", nodeId: fromNode.id, stateName: query2.stateName, typeName: query2.typeName, collectionName: query2.collectionName };
@@ -246243,6 +247088,10 @@ ${body}
246243
247088
  }
246244
247089
  if (fromNode.type === "Variable2" && fromProperty === "value") {
246245
247090
  const name = variableNameOf(fromNode);
247091
+ if (name !== void 0 && literalParam(fromNode, "runOnChange-variable") === false) {
247092
+ ctx.defer = VARIABLE_FROZEN;
247093
+ return null;
247094
+ }
246246
247095
  return name !== void 0 ? { kind: "store-get", variableName: name } : null;
246247
247096
  }
246248
247097
  if (fromNode.type === GLOBAL_STORE_SUBSCRIBE && fromProperty === "value") {
@@ -246296,6 +247145,9 @@ ${body}
246296
247145
  }
246297
247146
  }
246298
247147
  if ((RECORD_VERBS[fromNode.type] !== void 0 || RELATION_VERBS[fromNode.type] !== void 0 || USER_VERBS[fromNode.type] !== void 0) && fromProperty === "error") {
247148
+ if (verbChainScope.get(fromNode.id) === "failure") {
247149
+ return { kind: "outcome-error", nodeId: fromNode.id, local: `${verbErrorStateOf(fromNode).name}Message` };
247150
+ }
246299
247151
  if (!attachedRecordVerbs.has(fromNode.id)) {
246300
247152
  const trigger = USER_VERBS[fromNode.type]?.trigger ?? "store";
246301
247153
  const compiled2 = compiledOf(fromNode, trigger);
@@ -246992,6 +247844,72 @@ ${body}
246992
247844
  ctx.logicNodeIds.push(node.id);
246993
247845
  return output === "result" ? truthyExpr(cond) : notExpr(cond);
246994
247846
  };
247847
+ const SNAPSHOT_SINK_PORTS = /* @__PURE__ */ new Set(["mounted", "visible"]);
247848
+ const snapshotWiresOf = (node) => component.connections.filter((c) => c.fromId === node.id && (c.fromProperty === "result" || c.fromProperty === "isfalse"));
247849
+ let snapshotVerdictMemo;
247850
+ const snapshotVerdicts = () => {
247851
+ if (snapshotVerdictMemo !== void 0) return snapshotVerdictMemo;
247852
+ const verdicts = /* @__PURE__ */ new Map();
247853
+ for (const node of component.nodes) {
247854
+ if (node.type !== "Condition" || literalParam(node, "runOnChange-condition") !== false) continue;
247855
+ if (!wiredPorts.has(`${node.id}:eval`)) continue;
247856
+ const wires = snapshotWiresOf(node);
247857
+ if (wires.length === 0) continue;
247858
+ let reason2 = null;
247859
+ for (const w of wires) {
247860
+ const sink = nodeById.get(w.toId);
247861
+ if (!SNAPSHOT_SINK_PORTS.has(w.toProperty)) {
247862
+ reason2 = `its ${w.fromProperty} feeds ${sink?.type ?? "a missing node"}.${w.toProperty} \u2014 a snapshot of the last Evaluate lands only in a Mounted or a Visible in this slice`;
247863
+ } else if (!sink || !rendered2.has(sink.id)) {
247864
+ reason2 = `its ${w.fromProperty} feeds an element that is not drawn \u2014 a snapshot lands only in a drawn element's Mounted or Visible in this slice`;
247865
+ } else if (w.toProperty === "mounted" && sink.id === plan.rootId) {
247866
+ reason2 = "a mounted wire into the component root is a router concern \u2014 not translated in this slice";
247867
+ }
247868
+ if (reason2 !== null) break;
247869
+ }
247870
+ verdicts.set(node.id, reason2);
247871
+ }
247872
+ for (let changed = true; changed; ) {
247873
+ changed = false;
247874
+ for (const [id, verdict] of verdicts) {
247875
+ if (verdict !== null) continue;
247876
+ for (const w of snapshotWiresOf(nodeById.get(id))) {
247877
+ const feeders = component.connections.filter((c) => c.toId === w.toId && c.toProperty === w.toProperty);
247878
+ const isSnapshot = (c) => (c.fromProperty === "result" || c.fromProperty === "isfalse") && verdicts.has(c.fromId);
247879
+ const outsider = feeders.find((c) => !isSnapshot(c));
247880
+ const peer = feeders.find((c) => isSnapshot(c) && verdicts.get(c.fromId) !== null);
247881
+ if (outsider === void 0 && peer === void 0) continue;
247882
+ verdicts.set(
247883
+ id,
247884
+ outsider !== void 0 ? `${w.toId}.${w.toProperty} is also fed by ${nodeById.get(outsider.fromId)?.type ?? "a missing node"}.${outsider.fromProperty}, which is not an Evaluate snapshot this slice carries \u2014 the runtime shows whichever wrote last` : `${w.toId}.${w.toProperty} is also written by Condition ${peer.fromId}, which does not translate here \u2014 the runtime shows whichever wrote last`
247885
+ );
247886
+ changed = true;
247887
+ break;
247888
+ }
247889
+ }
247890
+ }
247891
+ snapshotVerdictMemo = verdicts;
247892
+ return verdicts;
247893
+ };
247894
+ const snapshotCells = /* @__PURE__ */ new Map();
247895
+ const snapshotCellOf = (toId, toProperty) => {
247896
+ const key2 = `${toId}\0${toProperty}`;
247897
+ let cell = snapshotCells.get(key2);
247898
+ if (cell === void 0) {
247899
+ const sink = nodeById.get(toId);
247900
+ cell = allocStateVar(
247901
+ sink?.authoredLabel === void 0 ? void 0 : `${sink.authoredLabel} ${toProperty}`,
247902
+ toProperty === "mounted" ? "shown" : "visible",
247903
+ "boolean",
247904
+ false,
247905
+ toId,
247906
+ "condition",
247907
+ `${toProperty === "mounted" ? "Mounted" : "Visible"} of ${sink?.authoredLabel ? `"${sink.authoredLabel}"` : toId}, written by the Evaluate of the Condition${component.connections.filter((c) => c.toId === toId && c.toProperty === toProperty).length > 1 ? "s" : ""} wired into it \u2014 false until the first one (EXP-011 \xA787).`
247908
+ );
247909
+ snapshotCells.set(key2, cell);
247910
+ }
247911
+ return cell;
247912
+ };
246995
247913
  const formatExprOf = (node, ctx) => {
246996
247914
  if (wiredPorts.has(`${node.id}:format`)) {
246997
247915
  ctx.defer = "the format string is wired, not literal";
@@ -247234,6 +248152,8 @@ ${body}
247234
248152
  [CLOUD_FUNCTION_TYPE2]: "call",
247235
248153
  // EXP-011 §43. `Record`'s only action port; with it unwired the node is an effect (§43.2).
247236
248154
  [RECORD_TYPE]: "fetch",
248155
+ // P18-S116-QUERYFETCH. `Query Records`' only action port (display name "Do"); with it unwired the node is an effect.
248156
+ DbCollection2: "storageFetch",
247237
248157
  // EXP-011 §45. The three files nodes' only action ports (`Cloud File` has none — it is a read).
247238
248158
  [FILE_PICKER_TYPE]: "open",
247239
248159
  [UPLOAD_FILE_TYPE]: "upload",
@@ -247309,6 +248229,14 @@ ${body}
247309
248229
  return taken.has("onClose") ? 'a declared port already claims the reserved prop "onClose" \u2014 rename the port (POPUPS-TARGET \xA74)' : void 0;
247310
248230
  })();
247311
248231
  const doneChainOf = (node, port5 = "done") => {
248232
+ chainSenders.push(node.id);
248233
+ try {
248234
+ return doneChainOfSender(node, port5);
248235
+ } finally {
248236
+ chainSenders.pop();
248237
+ }
248238
+ };
248239
+ const doneChainOfSender = (node, port5) => {
247312
248240
  const then2 = [];
247313
248241
  const consumes = [];
247314
248242
  const collapses = [];
@@ -247340,6 +248268,18 @@ ${body}
247340
248268
  }
247341
248269
  return { then: then2, consumes, collapses, subscribes };
247342
248270
  };
248271
+ const verbFailChainOf = (node) => {
248272
+ const outer = verbChainScope.get(node.id);
248273
+ verbChainScope.set(node.id, "failure");
248274
+ let chain;
248275
+ try {
248276
+ chain = doneChainOf(node, "failure");
248277
+ } finally {
248278
+ if (outer === void 0) verbChainScope.delete(node.id);
248279
+ else verbChainScope.set(node.id, outer);
248280
+ }
248281
+ return chain;
248282
+ };
247343
248283
  const compileShowPopup = (node) => {
247344
248284
  if (wiredPorts.has(`${node.id}:target`)) {
247345
248285
  return { defer: "target is wired \u2014 which component opens is not statically knowable" };
@@ -247455,9 +248395,65 @@ ${body}
247455
248395
  if (from?.type === "NewModel" && !chainByNewModel.has(from.id)) chainByNewModel.set(from.id, result);
247456
248396
  }
247457
248397
  }
248398
+ const pulseRefusals = /* @__PURE__ */ new Map();
248399
+ const pulseChainsOf = (node, def) => {
248400
+ const none = { then: [], consumes: [], collapses: [], subscribes: [] };
248401
+ if (def.kind !== "function") return none;
248402
+ const sentWires = component.connections.filter((c) => c.fromId === node.id && def.signals.includes(c.fromProperty.slice("out-".length)));
248403
+ if (sentWires.length === 0) return none;
248404
+ const signalsByPort = /* @__PURE__ */ new Map();
248405
+ for (const c of sentWires) {
248406
+ const port5 = `${c.toId}:${c.toProperty}`;
248407
+ const signals = signalsByPort.get(port5) ?? /* @__PURE__ */ new Set();
248408
+ signals.add(c.fromProperty.slice("out-".length));
248409
+ signalsByPort.set(port5, signals);
248410
+ }
248411
+ for (const [port5, signals] of signalsByPort) {
248412
+ if (signals.size < 2) continue;
248413
+ const target = nodeById.get(port5.slice(0, port5.indexOf(":")));
248414
+ return {
248415
+ defer: `two of its signals (${[...signals].map((s) => `"${s}"`).join(", ")}) drive ${target?.type ?? "a missing node"}.${port5.slice(port5.indexOf(":") + 1)} \u2014 which value each pulse reads there is not translated`
248416
+ };
248417
+ }
248418
+ const arms = [];
248419
+ const result = { then: [], consumes: [], collapses: [], subscribes: [] };
248420
+ for (const signal2 of def.signals) {
248421
+ if (!sentWires.some((c) => c.fromProperty === `out-${signal2}`)) continue;
248422
+ const arm = doneChainOf(node, `out-${signal2}`);
248423
+ if ("defer" in arm) {
248424
+ pulseRefusals.set(`${node.id}:out-${signal2}`, arm.defer.replace(`its out-${signal2} output`, `its "${signal2}" signal`));
248425
+ continue;
248426
+ }
248427
+ arms.push({ kind: "jsfun-pulse", nodeId: node.id, signal: signal2, then: arm.then });
248428
+ result.consumes.push(...arm.consumes);
248429
+ result.collapses.push(...arm.collapses);
248430
+ result.subscribes.push(...arm.subscribes);
248431
+ }
248432
+ if (arms.length > 0) result.then.push({ kind: "jsfun-pulses", nodeId: node.id, then: arms });
248433
+ return result;
248434
+ };
248435
+ const tickedWatchOf = (node, def) => {
248436
+ if (def.kind === "visual") return { watch: [] };
248437
+ const watch = [];
248438
+ for (const input of def.inputs) {
248439
+ if (input.expr === void 0) continue;
248440
+ if (literalParam(node, def.kind === "function" ? `runOnChange-in-${input.name}` : `runOnChange-${input.name}`) === false) continue;
248441
+ if (!exprValidIn(input.expr, { kind: "render" })) {
248442
+ return {
248443
+ defer: `its "${input.name}" input is ticked under Run On Value Change, so a new value runs it again, and that value exists only inside a handler \u2014 the export cannot watch it`
248444
+ };
248445
+ }
248446
+ watch.push(input.expr);
248447
+ }
248448
+ return { watch };
248449
+ };
248450
+ const isRowReader = (reader, port5) => reader.type === JS_FUNCTION && port5.startsWith("in-") || reader.type === "Set Variable" && port5 === "value" || reader.type === "DbModel2" && port5 === "modelId" || reader.type === "SetDbModelProperties" && (port5 === "modelId" || port5.startsWith("prop-")) || reader.type === "NewDbModelProperties" && port5.startsWith("prop-") || reader.type === "DeleteDbModelProperties" && port5 === "modelId" || reader.type === CLOUD_FUNCTION_TYPE2 && port5.startsWith("in-") || reader.type === "DbCollection2" && port5.startsWith("qp-");
248451
+ const tickedRunPieces = /* @__PURE__ */ new Map();
247458
248452
  const compileJsRun = (node) => {
247459
248453
  const record2 = jsFunDefOf(node);
247460
248454
  if ("defer" in record2) return { defer: record2.defer };
248455
+ const ticked3 = tickedWatchOf(node, record2.def);
248456
+ if ("defer" in ticked3) return { defer: ticked3.defer };
247461
248457
  for (const c of component.connections) {
247462
248458
  if (c.fromId !== node.id || c.fromProperty === "done" || isJsValueOutput(node, c.fromProperty)) continue;
247463
248459
  if (node.type === JS_FUNCTION && !c.fromProperty.startsWith("out-")) {
@@ -247472,19 +248468,68 @@ ${body}
247472
248468
  }
247473
248469
  return { defer: `its ${c.fromProperty} output is consumed \u2014 only done continues a Run chain in this slice` };
247474
248470
  }
247475
- const chain = doneChainOf(node);
247476
- if ("defer" in chain) return chain;
247477
- const strayRead = component.connections.find(
248471
+ const done = doneChainOf(node);
248472
+ if ("defer" in done) return done;
248473
+ const pulsed = pulseChainsOf(node, record2.def);
248474
+ if ("defer" in pulsed) pulseRefusals.set(node.id, pulsed.defer);
248475
+ const pulses = "defer" in pulsed ? { then: [], consumes: [], collapses: [], subscribes: [] } : pulsed;
248476
+ if (ticked3.watch.length > 0 && !actionsValidIn([{ kind: "jsfun-run", nodeId: node.id, then: pulses.then }], { kind: "render" })) {
248477
+ return {
248478
+ defer: "a ticked input runs it again from no handler, and its signals' chains read values that exist only inside a handler"
248479
+ };
248480
+ }
248481
+ const chain = {
248482
+ then: [...pulses.then, ...done.then],
248483
+ consumes: [...pulses.consumes, ...done.consumes],
248484
+ collapses: [...pulses.collapses, ...done.collapses],
248485
+ subscribes: [...pulses.subscribes, ...done.subscribes]
248486
+ };
248487
+ const strays = component.connections.filter(
247478
248488
  (c) => c.fromId === node.id && isJsValueOutput(node, c.fromProperty) && !chain.consumes.includes(c.key)
247479
248489
  );
247480
- let materialize;
247481
- if (strayRead) {
247482
- const strayRendered = nodeById.get(strayRead.toId);
247483
- if (!strayRendered || !rendered2.has(strayRead.toId)) {
248490
+ const signalTargetOf = (toId) => record2.def.kind !== "function" ? void 0 : component.connections.find((c) => c.fromId === node.id && c.toId === toId && record2.def.signals.includes(c.fromProperty.slice("out-".length)));
248491
+ const missingSink = strays.find((c) => nodeById.get(c.toId) === void 0);
248492
+ if (missingSink) {
248493
+ return { defer: `its ${missingSink.fromProperty} output is consumed outside the Run chain by a missing node` };
248494
+ }
248495
+ const ownSignalStray = (c) => {
248496
+ if (rendered2.has(c.toId)) return false;
248497
+ const signalWire = signalTargetOf(c.toId);
248498
+ return signalWire !== void 0 && (pulseRefusals.has(`${node.id}:${signalWire.fromProperty}`) || pulseRefusals.has(node.id));
248499
+ };
248500
+ const headMaterialized = strays.length > 0 && rendered2.has(strays[0].toId);
248501
+ const unreadable = headMaterialized ? void 0 : strays.find(
248502
+ (c) => !rendered2.has(c.toId) && !ownSignalStray(c) && (record2.def.kind !== "function" || !isRowReader(nodeById.get(c.toId), c.toProperty))
248503
+ );
248504
+ if (unreadable) {
248505
+ return {
248506
+ defer: `its ${unreadable.fromProperty} output is consumed outside the Run chain by an unrendered sink \u2014 the last run's value is not statically expressible there`
248507
+ };
248508
+ }
248509
+ const readers = headMaterialized ? strays : strays.filter((c) => !ownSignalStray(c));
248510
+ const runWires = component.connections.filter((c) => c.toId === node.id && c.toProperty === "run");
248511
+ for (const reader of readers) {
248512
+ if (rendered2.has(reader.toId)) continue;
248513
+ const early = component.connections.find(
248514
+ (w, i) => w.toId === reader.toId && runWires.some((r) => r.fromId === w.fromId && r.fromProperty === w.fromProperty && i < component.connections.indexOf(r))
248515
+ );
248516
+ if (early) {
247484
248517
  return {
247485
- defer: `its ${strayRead.fromProperty} output is consumed outside the Run chain by an unrendered sink \u2014 the last run's value is not statically expressible there`
248518
+ defer: `its ${reader.fromProperty} output is read by ${nodeById.get(reader.toId).type}.${reader.toProperty}, whose ${early.toProperty} the same ${early.fromProperty} sends before it runs this \u2014 the runtime's reader waits for the run's value, the export's would read the one before`
247486
248519
  };
247487
248520
  }
248521
+ }
248522
+ if (readers.length === 0 && strays.length > 0 && chain.then.length === 0) {
248523
+ const stray = strays[0];
248524
+ const signalWire = signalTargetOf(stray.toId);
248525
+ const refused = pulseRefusals.get(`${node.id}:${signalWire.fromProperty}`) ?? pulseRefusals.get(node.id);
248526
+ return {
248527
+ defer: refused !== void 0 ? `its ${stray.fromProperty} output feeds ${nodeById.get(stray.toId).type}.${stray.toProperty}, which its "${signalWire.fromProperty.slice("out-".length)}" signal runs, and that chain is not exported: ${refused}` : `its ${stray.fromProperty} output is consumed outside the Run chain by an unrendered sink \u2014 the last run's value is not statically expressible there`
248528
+ };
248529
+ }
248530
+ const strayRead = readers[0];
248531
+ let materialize;
248532
+ if (strayRead) {
247488
248533
  const def = record2.def;
247489
248534
  const fields = def.outputs.map(
247490
248535
  (o) => `${/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(o.name) ? o.name : JSON.stringify(o.name)}?: ${o.tsType}`
@@ -247505,6 +248550,13 @@ ${body}
247505
248550
  if (chain.then.length === 0 && materialize === void 0) {
247506
248551
  return { defer: "its Run drives nothing this slice translates \u2014 no done-chain action consumes its work" };
247507
248552
  }
248553
+ if (pulses.then.length > 0 || materialize !== void 0 && record2.def.kind === "function") record2.def.pulsed = true;
248554
+ if (ticked3.watch.length > 0 && (pulses.then.length > 0 || materialize !== void 0)) {
248555
+ tickedRunPieces.set(node.id, {
248556
+ watch: ticked3.watch,
248557
+ action: { kind: "jsfun-run", nodeId: node.id, then: pulses.then, ...materialize !== void 0 ? { materialize } : {}, ticked: true }
248558
+ });
248559
+ }
247508
248560
  return {
247509
248561
  action: {
247510
248562
  kind: "jsfun-run",
@@ -247576,6 +248628,18 @@ ${body}
247576
248628
  if (literalParam(node, "idSource") === "foreach" || authoredOrWired("repeaterComponent")) {
247577
248629
  return { defer: "its Id Source is the enclosing repeater's row \u2014 row identity is not statically knowable in this slice" };
247578
248630
  }
248631
+ const namesProperties = (name) => {
248632
+ const value = literalParam(node, name);
248633
+ if (wiredPorts.has(`${node.id}:${name}`)) return true;
248634
+ if (typeof value !== "string") return false;
248635
+ return name === "upsertOn" ? value.trim() !== "" : value.split(",").some((n) => n.trim() !== "");
248636
+ };
248637
+ if (verb === "create" && namesProperties("upsertOn")) {
248638
+ return { defer: "its Upsert On names a unique property \u2014 a clash would update the existing row in the runtime, and the exported create always creates" };
248639
+ }
248640
+ if (verb === "update" && namesProperties("onlyIfUnchanged")) {
248641
+ return { defer: "its Only If Unchanged names properties \u2014 the runtime writes only if they still hold what the record was read with, and the exported update writes unconditionally" };
248642
+ }
247579
248643
  if (verb === "update") {
247580
248644
  if (literalParam(node, "storeType") === "local" || wiredPorts.has(`${node.id}:storeType`)) {
247581
248645
  return { defer: "Store to is Local only \u2014 an in-memory-only write, and the export holds no record to write into" };
@@ -247591,8 +248655,8 @@ ${body}
247591
248655
  };
247592
248656
  }
247593
248657
  for (const wire of component.connections.filter((c) => c.fromId === node.id)) {
247594
- if (wire.fromProperty === "failure" || wire.fromProperty === "completed") {
247595
- return { defer: `its ${wire.fromProperty} output is consumed \u2014 only the done chain and the Error value are translated in this slice` };
248658
+ if (wire.fromProperty === "completed") {
248659
+ return { defer: `its ${wire.fromProperty} output is consumed \u2014 only the done and failure chains and the Error value are translated in this slice` };
247596
248660
  }
247597
248661
  if (wire.fromProperty === "id") {
247598
248662
  return { defer: "its Id output is consumed \u2014 the record it names exists only inside the invoking chain, which the relation verbs would need" };
@@ -247649,6 +248713,8 @@ ${body}
247649
248713
  }
247650
248714
  const chain = doneChainOf(node);
247651
248715
  if ("defer" in chain) return { defer: chain.defer };
248716
+ const failChain = verbFailChainOf(node);
248717
+ if ("defer" in failChain) return { defer: failChain.defer };
247652
248718
  const { typeName: typeName3, moduleBase } = collectionModuleNames(collectionName4);
247653
248719
  const fnName = `${verb}${typeName3}`;
247654
248720
  plan.mutations.push({
@@ -247678,11 +248744,12 @@ ${body}
247678
248744
  // EXP-011 §72. An `unknown` id (an untyped Variable, a store key nothing types) into a `string` parameter.
247679
248745
  ...idExpr !== void 0 && idExpr.kind !== "literal" && exprTsType(idExpr) === "unknown" ? { coerceId: "string" } : {},
247680
248746
  errorState: verbErrorStateOf(node).name,
247681
- then: chain.then
248747
+ then: chain.then,
248748
+ failThen: failChain.then
247682
248749
  },
247683
- consumes: [...consumes, ...chain.consumes, ...ctx.consumes],
247684
- collapses: [...ctx.logicNodeIds, ...chain.collapses],
247685
- subscribes: [...ctx.subscriberIds, ...chain.subscribes]
248750
+ consumes: [...consumes, ...chain.consumes, ...failChain.consumes, ...ctx.consumes],
248751
+ collapses: [...ctx.logicNodeIds, ...chain.collapses, ...failChain.collapses],
248752
+ subscribes: [...ctx.subscriberIds, ...chain.subscribes, ...failChain.subscribes]
247686
248753
  };
247687
248754
  };
247688
248755
  const compileRelationOp = (node) => {
@@ -247701,8 +248768,8 @@ ${body}
247701
248768
  return { defer: "its Id Source is the enclosing repeater's row \u2014 row identity is not statically knowable in this slice" };
247702
248769
  }
247703
248770
  for (const wire of component.connections.filter((c) => c.fromId === node.id)) {
247704
- if (wire.fromProperty === "failure" || wire.fromProperty === "completed") {
247705
- return { defer: `its ${wire.fromProperty} output is consumed \u2014 only the done chain and the Error value are translated in this slice` };
248771
+ if (wire.fromProperty === "completed") {
248772
+ return { defer: `its ${wire.fromProperty} output is consumed \u2014 only the done and failure chains and the Error value are translated in this slice` };
247706
248773
  }
247707
248774
  if (wire.fromProperty === "id") {
247708
248775
  return { defer: "its Id output is consumed \u2014 it republishes the Id it was given, and that read is not translated in this slice" };
@@ -247752,6 +248819,8 @@ ${body}
247752
248819
  consumes.push(targetWires[0].key);
247753
248820
  const chain = doneChainOf(node);
247754
248821
  if ("defer" in chain) return { defer: chain.defer };
248822
+ const failChain = verbFailChainOf(node);
248823
+ if ("defer" in failChain) return { defer: failChain.defer };
247755
248824
  const { typeName: typeName3, moduleBase } = collectionModuleNames(collectionName4);
247756
248825
  const fnName = `${spec.fnPrefix}${typeName3}Relation`;
247757
248826
  plan.mutations.push({ nodeId: node.id, verb: spec.verb, collectionName: collectionName4, fnName, typeName: typeName3, moduleBase, writes: [] });
@@ -247776,11 +248845,12 @@ ${body}
247776
248845
  guardId: false,
247777
248846
  ...guards.length > 0 ? { guards } : {},
247778
248847
  errorState: verbErrorStateOf(node).name,
247779
- then: chain.then
248848
+ then: chain.then,
248849
+ failThen: failChain.then
247780
248850
  },
247781
- consumes: [...consumes, ...chain.consumes, ...ctx.consumes],
247782
- collapses: [...ctx.logicNodeIds, ...chain.collapses],
247783
- subscribes: [...ctx.subscriberIds, ...chain.subscribes]
248851
+ consumes: [...consumes, ...chain.consumes, ...failChain.consumes, ...ctx.consumes],
248852
+ collapses: [...ctx.logicNodeIds, ...chain.collapses, ...failChain.collapses],
248853
+ subscribes: [...ctx.subscriberIds, ...chain.subscribes, ...failChain.subscribes]
247784
248854
  };
247785
248855
  };
247786
248856
  const compileUserOp = (node) => {
@@ -247788,9 +248858,9 @@ ${body}
247788
248858
  const ctx = newCtx();
247789
248859
  const consumes = [];
247790
248860
  for (const wire of component.connections.filter((c) => c.fromId === node.id)) {
247791
- if (wire.fromProperty === "failure" || wire.fromProperty === "completed") {
248861
+ if (wire.fromProperty === "completed") {
247792
248862
  return {
247793
- defer: `its ${wire.fromProperty} output is consumed \u2014 only the done chain and the Error value are translated in this slice`
248863
+ defer: `its ${wire.fromProperty} output is consumed \u2014 only the done and failure chains and the Error value are translated in this slice`
247794
248864
  };
247795
248865
  }
247796
248866
  }
@@ -247850,6 +248920,8 @@ ${body}
247850
248920
  }
247851
248921
  const chain = doneChainOf(node);
247852
248922
  if ("defer" in chain) return { defer: chain.defer };
248923
+ const failChain = verbFailChainOf(node);
248924
+ if ("defer" in failChain) return { defer: failChain.defer };
247853
248925
  const call5 = { nodeId: node.id, verb: spec.verb, fnName: spec.fnName };
247854
248926
  if (spec.columns === true) {
247855
248927
  call5.writes = columns.map(({ key: key2, expr }) => {
@@ -247874,11 +248946,12 @@ ${body}
247874
248946
  guardId: false,
247875
248947
  errorState: verbErrorStateOf(node).name,
247876
248948
  ...spec.clearsErrorOnDone === true ? { clearErrorOnDone: true } : {},
247877
- then: chain.then
248949
+ then: chain.then,
248950
+ failThen: failChain.then
247878
248951
  },
247879
- consumes: [...consumes, ...chain.consumes, ...ctx.consumes],
247880
- collapses: [...ctx.logicNodeIds, ...chain.collapses],
247881
- subscribes: [...ctx.subscriberIds, ...chain.subscribes]
248952
+ consumes: [...consumes, ...chain.consumes, ...failChain.consumes, ...ctx.consumes],
248953
+ collapses: [...ctx.logicNodeIds, ...chain.collapses, ...failChain.collapses],
248954
+ subscribes: [...ctx.subscriberIds, ...chain.subscribes, ...failChain.subscribes]
247882
248955
  };
247883
248956
  };
247884
248957
  const compileHttpFetch = (node) => {
@@ -247930,7 +249003,7 @@ ${body}
247930
249003
  const ctx = newCtx();
247931
249004
  const args = [];
247932
249005
  const takenParams = /* @__PURE__ */ new Set();
247933
- const valueOf = (port5, name, paramBase) => {
249006
+ const valueOf2 = (port5, name, paramBase) => {
247934
249007
  const wires = component.connections.filter((c) => c.toId === node.id && c.toProperty === port5);
247935
249008
  if (wires.length > 1) {
247936
249009
  return { defer: `two wires feed ${port5} \u2014 last-writer-wins is not statically ordered` };
@@ -247957,7 +249030,7 @@ ${body}
247957
249030
  const stringList = (param) => String(literalParam(node, param) ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
247958
249031
  const collect3 = (names3, portPrefix, paramPrefix, into) => {
247959
249032
  for (const name of names3) {
247960
- const value = valueOf(`${portPrefix}${name}`, name, `${paramPrefix}${pascalCase2(name.replace(/[^A-Za-z0-9]+/g, " "))}`);
249033
+ const value = valueOf2(`${portPrefix}${name}`, name, `${paramPrefix}${pascalCase2(name.replace(/[^A-Za-z0-9]+/g, " "))}`);
247961
249034
  if (isDefer(value)) return value;
247962
249035
  if (value !== null) into.push(value);
247963
249036
  }
@@ -247979,7 +249052,7 @@ ${body}
247979
249052
  if (["POST", "PUT", "PATCH"].includes(method)) {
247980
249053
  const bodyType = String(literalParam(node, "bodyType") ?? "json");
247981
249054
  if (bodyType === "raw") {
247982
- const raw = valueOf("body-raw", "raw", "bodyRaw");
249055
+ const raw = valueOf2("body-raw", "raw", "bodyRaw");
247983
249056
  if (isDefer(raw)) return raw;
247984
249057
  if (raw !== null) body = { type: "raw", raw };
247985
249058
  } else if (bodyType === "json" || bodyType === "form" || bodyType === "urlencoded") {
@@ -247994,19 +249067,19 @@ ${body}
247994
249067
  let auth = null;
247995
249068
  const authType = String(literalParam(node, "authType") ?? "none");
247996
249069
  if (authType === "bearer") {
247997
- const token = valueOf("auth-authToken", "authToken", "authToken");
249070
+ const token = valueOf2("auth-authToken", "authToken", "authToken");
247998
249071
  if (isDefer(token)) return token;
247999
249072
  if (token !== null) auth = { kind: "bearer", token };
248000
249073
  } else if (authType === "basic") {
248001
- const username = valueOf("auth-authUsername", "authUsername", "authUsername");
249074
+ const username = valueOf2("auth-authUsername", "authUsername", "authUsername");
248002
249075
  if (isDefer(username)) return username;
248003
- const password = valueOf("auth-authPassword", "authPassword", "authPassword");
249076
+ const password = valueOf2("auth-authPassword", "authPassword", "authPassword");
248004
249077
  if (isDefer(password)) return password;
248005
249078
  if (username !== null && password !== null) auth = { kind: "basic", username, password };
248006
249079
  } else if (authType === "apiKey") {
248007
- const name = valueOf("auth-authApiKeyName", "authApiKeyName", "authKeyName");
249080
+ const name = valueOf2("auth-authApiKeyName", "authApiKeyName", "authKeyName");
248008
249081
  if (isDefer(name)) return name;
248009
- const value = valueOf("auth-authApiKeyValue", "authApiKeyValue", "authKeyValue");
249082
+ const value = valueOf2("auth-authApiKeyValue", "authApiKeyValue", "authKeyValue");
248010
249083
  if (isDefer(value)) return value;
248011
249084
  if (name !== null && value !== null) {
248012
249085
  auth = {
@@ -248408,7 +249481,9 @@ ${body}
248408
249481
  answerLocal: names2.answerLocal,
248409
249482
  messageLocal: names2.messageLocal,
248410
249483
  idLocal: names2.idLocal,
248411
- reads
249484
+ reads,
249485
+ // With no backend the read is a stub that throws, the row is never set, and there is no client to hold a copy.
249486
+ live: ir.project.cloudservices !== void 0 && literalParam(node, "runOnChange-record") !== false
248412
249487
  });
248413
249488
  return {
248414
249489
  action: {
@@ -248428,6 +249503,16 @@ ${body}
248428
249503
  subscribes: [...ctx.subscriberIds, ...chain.subscribes, ...failChain.subscribes]
248429
249504
  };
248430
249505
  };
249506
+ const compileQueryFetch = (node) => {
249507
+ const settings = querySettingsFor(node);
249508
+ if ("refuse" in settings) return { defer: `the Query Records it fetches is not exported: ${settings.refuse}` };
249509
+ return {
249510
+ action: { kind: "query-fetch", nodeId: node.id, values: queryWirings.get(node.id)?.values ?? [] },
249511
+ consumes: [],
249512
+ collapses: [],
249513
+ subscribes: []
249514
+ };
249515
+ };
248431
249516
  const fileOutputsRefusal = (node, valueOutputs, unchanged) => {
248432
249517
  for (const wire of component.connections.filter((c) => c.fromId === node.id)) {
248433
249518
  const p = wire.fromProperty;
@@ -249088,6 +250173,10 @@ ${body}
249088
250173
  const compileRouterNavigate = (node, page) => {
249089
250174
  const ctx = newCtx();
249090
250175
  const consumes = [];
250176
+ if (wiredPorts.has(`${node.id}:openInNewTab`)) {
250177
+ return { defer: "its Open In New Tab port is wired \u2014 whether it opens a tab or navigates in place is a runtime value" };
250178
+ }
250179
+ const newTab = literalParam(node, "openInNewTab") === true;
249091
250180
  const values = /* @__PURE__ */ new Map();
249092
250181
  for (const param of node.parameters) {
249093
250182
  if (!param.name.startsWith("pm-")) continue;
@@ -249117,7 +250206,7 @@ ${body}
249117
250206
  }
249118
250207
  const query2 = Array.from(values, ([name, expr]) => ({ name, expr }));
249119
250208
  return {
249120
- action: { kind: "navigate", to: page.urlPath, pathParams, query: query2 },
250209
+ action: { kind: "navigate", to: page.urlPath, pathParams, query: query2, ...newTab ? { newTab: true } : {} },
249121
250210
  consumes: [...consumes, ...ctx.consumes],
249122
250211
  collapses: ctx.logicNodeIds,
249123
250212
  subscribes: ctx.subscriberIds
@@ -249216,7 +250305,7 @@ ${body}
249216
250305
  const level = LOG_LEVELS.includes(String(authoredLevel)) ? authoredLevel : "info";
249217
250306
  const ctx = newCtx();
249218
250307
  const consumes = [];
249219
- const valueOf = (port5) => {
250308
+ const valueOf2 = (port5) => {
249220
250309
  const wires = component.connections.filter((c) => c.toId === node.id && c.toProperty === port5);
249221
250310
  if (wires.length > 1) return { defer: `two wires feed its ${port5} input \u2014 last-writer-wins is not statically ordered` };
249222
250311
  if (wires.length === 1) {
@@ -249231,9 +250320,9 @@ ${body}
249231
250320
  const literal2 = literalParam(node, port5);
249232
250321
  return literal2 === void 0 ? void 0 : { kind: "literal", value: literal2 };
249233
250322
  };
249234
- const message2 = valueOf("message");
250323
+ const message2 = valueOf2("message");
249235
250324
  if (isDefer(message2)) return message2;
249236
- const data = valueOf("data");
250325
+ const data = valueOf2("data");
249237
250326
  if (isDefer(data)) return data;
249238
250327
  const done = doneChainOf(node, "done");
249239
250328
  if ("defer" in done) return { defer: done.defer };
@@ -249822,6 +250911,10 @@ ${body}
249822
250911
  for (const key2 of consumes) consumed.add(key2);
249823
250912
  for (const id of collapses) dispositions[id] = { kind: "collapsed", into };
249824
250913
  for (const id of subscribes) boundSubscribers.add(id);
250914
+ const errorPort = outputPorts.find((p) => p.name === contract.error);
250915
+ if (errorPort !== void 0) {
250916
+ notes.push(`node ${node.id} (${RUN_TASKS_TYPE}): a failed task is reported without its Template's "${contract.error}" output \u2014 the runtime puts that value in the run-tasks/task-failed error, and the exported host has no value channel from a task`);
250917
+ }
249825
250918
  return core;
249826
250919
  };
249827
250920
  const compileRunTasks = (node, port5) => {
@@ -250204,12 +251297,14 @@ ${body}
250204
251297
  if (!Array.isArray(snapped)) return refuse(snapped.defer);
250205
251298
  arrive2 = snapped;
250206
251299
  }
251300
+ const castToNumber = ["duration", "delay"].filter((input) => exprTsType(input === "duration" ? duration3 : delay) !== "number");
250207
251301
  const registered = {
250208
251302
  nodeId: node.id,
250209
251303
  local: mintLocal(animateLocals, node, "Animated", ""),
250210
251304
  target,
250211
251305
  duration: duration3,
250212
251306
  delay,
251307
+ ...castToNumber.length > 0 ? { castToNumber: [...castToNumber] } : {},
250213
251308
  ease: easeParam,
250214
251309
  ...arrive2 !== void 0 ? { arrive: arrive2 } : {},
250215
251310
  comment: `${node.authoredLabel ?? "Animate To Value"} \u2014 moves towards its Target Value over Duration after Delay (animate-to-value.ts); the first target is adopted outright.`
@@ -250270,7 +251365,7 @@ ${body}
250270
251365
  consumes.push(wire.key);
250271
251366
  }
250272
251367
  }
250273
- const valueOf = (port5) => {
251368
+ const valueOf2 = (port5) => {
250274
251369
  let expr;
250275
251370
  const authored = node.parameters.find((param) => param.name === port5);
250276
251371
  if (authored?.value.kind === "literal") expr = { kind: "literal", value: authored.value.value };
@@ -250295,7 +251390,7 @@ ${body}
250295
251390
  }
250296
251391
  let expr = resolvedParams.get(name);
250297
251392
  if (expr === void 0) {
250298
- const resolved = valueOf(`p-${name}`);
251393
+ const resolved = valueOf2(`p-${name}`);
250299
251394
  if (isDefer(resolved)) return { defer: resolved.defer };
250300
251395
  expr = resolved ?? { kind: "literal", value: "" };
250301
251396
  resolvedParams.set(name, expr);
@@ -250306,7 +251401,7 @@ ${body}
250306
251401
  const query2 = [];
250307
251402
  if (typeof authoredQueryNames === "string" && authoredQueryNames !== "") {
250308
251403
  for (const name of authoredQueryNames.split(",")) {
250309
- const resolved = valueOf(`q-${name}`);
251404
+ const resolved = valueOf2(`q-${name}`);
250310
251405
  if (isDefer(resolved)) return { defer: resolved.defer };
250311
251406
  if (resolved === void 0) continue;
250312
251407
  query2.push({ name, expr: resolved, omittable: maybeUndefinedExpr(resolved) });
@@ -250423,7 +251518,10 @@ ${body}
250423
251518
  if (keyType === "number" || keyType === "boolean") {
250424
251519
  return { defer: `key "${key2}" is ${keyType}-typed by the initial state; only string writes translate in this slice` };
250425
251520
  }
250426
- const wire2 = component.connections.find((c) => c.toId === node.id && c.toProperty === "value");
251521
+ const wire2 = lastValueWireOf(node);
251522
+ if (wire2 === void 0 && valueWiresInto(node).length > 1) {
251523
+ return { defer: "two wires feed its Value input \u2014 last-writer-wins is not statically ordered" };
251524
+ }
250427
251525
  if (!wire2) {
250428
251526
  const authored = literalParam(node, "value");
250429
251527
  if (authored === void 0) return { defer: "nothing is wired into value" };
@@ -250492,6 +251590,7 @@ ${body}
250492
251590
  if (node.type === HTTP_TYPE) return compileHttpFetch(node);
250493
251591
  if (node.type === CLOUD_FUNCTION_TYPE2) return compileCloudCall(node);
250494
251592
  if (node.type === RECORD_TYPE) return compileRecordFetch(node);
251593
+ if (node.type === "DbCollection2") return compileQueryFetch(node);
250495
251594
  if (node.type === FILE_PICKER_TYPE) return compileFilePick(node);
250496
251595
  if (node.type === UPLOAD_FILE_TYPE) return compileFileUpload(node);
250497
251596
  if (node.type === SIGN_FILE_URL_TYPE) return compileFileSign(node);
@@ -250505,13 +251604,16 @@ ${body}
250505
251604
  const variableName = variableNameOf(node);
250506
251605
  if (variableName === void 0) return { defer: "variable name is not a literal" };
250507
251606
  const setWith = literalParam(node, "setWith");
250508
- const wire = component.connections.find((c) => c.toId === node.id && c.toProperty === "value");
251607
+ const wire = lastValueWireOf(node);
250509
251608
  if (setWith === "object" || setWith === "array") {
250510
251609
  return { defer: `setWith "${String(setWith)}" conversion is not translated in step 5` };
250511
251610
  }
250512
251611
  if (setWith === "emptyString") {
250513
251612
  return { action: { kind: "store-set", variableName, expr: { kind: "literal", value: "" } }, consumes: wire ? [wire.key] : [], collapses: [], subscribes: [] };
250514
251613
  }
251614
+ if (wire === void 0 && valueWiresInto(node).length > 1) {
251615
+ return { defer: "two wires feed its Value input \u2014 last-writer-wins is not statically ordered" };
251616
+ }
250515
251617
  if (setWith === "boolean" && wire) {
250516
251618
  return { defer: `setWith "boolean" conversion is not translated in step 5` };
250517
251619
  }
@@ -250694,10 +251796,17 @@ ${body}
250694
251796
  return compileConditionBranch(node);
250695
251797
  };
250696
251798
  const compileConditionBranch = (node) => {
251799
+ const snapshotVerdict = snapshotVerdicts().get(node.id);
251800
+ const snapshotWires = snapshotVerdict === null ? snapshotWiresOf(node) : [];
250697
251801
  const stray = component.connections.find(
250698
- (c) => c.fromId === node.id && c.fromProperty !== "ontrue" && c.fromProperty !== "onfalse"
251802
+ (c) => c.fromId === node.id && c.fromProperty !== "ontrue" && c.fromProperty !== "onfalse" && !snapshotWires.includes(c)
250699
251803
  );
250700
- if (stray) return { defer: `its ${stray.fromProperty} output drives logic this slice does not translate` };
251804
+ if (stray) {
251805
+ const valueOut = stray.fromProperty === "result" || stray.fromProperty === "isfalse";
251806
+ return {
251807
+ defer: valueOut && typeof snapshotVerdict === "string" ? snapshotVerdict : `its ${stray.fromProperty} output drives logic this slice does not translate`
251808
+ };
251809
+ }
250701
251810
  const ctx = newCtx();
250702
251811
  let cond;
250703
251812
  const condWire = component.connections.find((c) => c.toId === node.id && c.toProperty === "condition");
@@ -250748,6 +251857,25 @@ ${body}
250748
251857
  if ("defer" in whenTrue) return whenTrue;
250749
251858
  const whenFalse = arm("onfalse");
250750
251859
  if ("defer" in whenFalse) return whenFalse;
251860
+ if (snapshotWires.length > 0) {
251861
+ const writes = (answer3) => snapshotWires.map((w) => ({
251862
+ kind: "state-set",
251863
+ name: snapshotCellOf(w.toId, w.toProperty).name,
251864
+ expr: { kind: "literal", value: w.fromProperty === "result" ? answer3 : !answer3 }
251865
+ }));
251866
+ const truth = truthyExpr(cond);
251867
+ if (truth.kind === "literal") {
251868
+ const side = [...writes(truth.value === true), ...truth.value === true ? whenTrue : whenFalse];
251869
+ const action = side.length === 1 ? side[0] : { kind: "branch", cond: { kind: "literal", value: true }, whenTrue: side, whenFalse: [], flat: true };
251870
+ return { action, consumes, collapses, subscribes };
251871
+ }
251872
+ return {
251873
+ action: { kind: "branch", cond, whenTrue: [...writes(true), ...whenTrue], whenFalse: [...writes(false), ...whenFalse] },
251874
+ consumes,
251875
+ collapses,
251876
+ subscribes
251877
+ };
251878
+ }
250751
251879
  if (whenTrue.length === 0 && whenFalse.length === 0) {
250752
251880
  return { defer: "neither branch drives a translatable action" };
250753
251881
  }
@@ -250758,14 +251886,35 @@ ${body}
250758
251886
  const recordEffectDefers = /* @__PURE__ */ new Map();
250759
251887
  const compiling = /* @__PURE__ */ new Set();
250760
251888
  const compiledOf = (node, port5) => {
250761
- const key2 = `${node.id}:${port5}`;
250762
- const cached3 = compiledSinks.get(key2);
250763
- if (cached3 !== void 0) return cached3;
251889
+ const valueWires = (node.type === "Set Variable" || node.type === GLOBAL_STORE_SET) && valueWiresInto(node).length > 1;
251890
+ const plainKey = `${node.id}:${port5}${valueWires ? `@value:${lastValueWireOf(node)?.key ?? "none"}` : ""}`;
251891
+ const scoped = [...verbChainScope].filter(([, s]) => s === "failure").map(([id]) => id);
251892
+ const scopeKey = scoped.length > 0 ? `${plainKey}@failure:${[...scoped].sort().join(",")}` : void 0;
251893
+ const readsScoped = (result2, form) => {
251894
+ if ("defer" in result2 || scoped.length === 0) return false;
251895
+ const rows = form === "row" ? new Set(scoped.map((id) => verbErrorVars.get(id)?.name).filter((n) => n !== void 0)) : void 0;
251896
+ let found2 = false;
251897
+ const visit = (value) => {
251898
+ if (found2 || value === null || typeof value !== "object") return;
251899
+ if (Array.isArray(value)) return value.forEach(visit);
251900
+ const v = value;
251901
+ if (form === "local" && v.kind === "outcome-error" && v.viaState === void 0 && scoped.includes(v.nodeId)) found2 = true;
251902
+ if (form === "row" && v.kind === "state-get" && rows.has(v.name)) found2 = true;
251903
+ for (const child of Object.values(value)) visit(child);
251904
+ };
251905
+ visit(result2.action);
251906
+ return found2;
251907
+ };
251908
+ const cachedScoped = scopeKey !== void 0 ? compiledSinks.get(scopeKey) : void 0;
251909
+ if (cachedScoped !== void 0) return cachedScoped;
251910
+ const cached3 = compiledSinks.get(plainKey);
251911
+ if (cached3 !== void 0 && !readsScoped(cached3, "row")) return cached3;
251912
+ const key2 = scopeKey !== void 0 && (cached3 !== void 0 || compiling.has(plainKey)) ? scopeKey : plainKey;
250764
251913
  if (compiling.has(key2)) return { defer: "its trigger chain is cyclic" };
250765
251914
  compiling.add(key2);
250766
251915
  const result = compileSink(node, port5);
250767
251916
  compiling.delete(key2);
250768
- compiledSinks.set(key2, result);
251917
+ compiledSinks.set(key2 === plainKey && readsScoped(result, "local") ? scopeKey : key2, result);
250769
251918
  return result;
250770
251919
  };
250771
251920
  const exprValidIn = (expr, context, invokedScope) => {
@@ -250947,7 +252096,7 @@ ${body}
250947
252096
  case "api-call":
250948
252097
  return action.args.every(
250949
252098
  (arg) => arg.kind === "expr" ? exprValidIn(arg.expr, context, invokedScope) : arg.props.every((p) => exprValidIn(p.expr, context, invokedScope))
250950
- ) && actionsValidIn(action.then, context, invokedScope);
252099
+ ) && actionsValidIn(action.then, context, invokedScope) && actionsValidIn(action.failThen, context, invokedScope);
250951
252100
  case "jsfun-run": {
250952
252101
  const def = plan.jsFunctions[action.nodeId];
250953
252102
  if (def === void 0) return false;
@@ -251069,6 +252218,13 @@ ${body}
251069
252218
  return exprValidIn(action.items, context, invokedScope);
251070
252219
  case "runtasks-abort":
251071
252220
  return true;
252221
+ // P18-S116-QUERYFETCH. The wired filter values are read at the pulse, in the handler that pulses.
252222
+ case "query-fetch":
252223
+ return action.values.every((v) => exprValidIn(v.expr, context, invokedScope));
252224
+ // EXP-011 §86. A run's pulse loop and its arms sit inside the run's own `then`, so its scope already holds the node.
252225
+ case "jsfun-pulses":
252226
+ case "jsfun-pulse":
252227
+ return actionsValidIn(action.then, context, invokedScope);
251072
252228
  // EXP-011 §58. The data is read at the pulse, in the handler that pulses; the listeners are validated where they compile.
251073
252229
  case "stream-action":
251074
252230
  return action.value === void 0 || exprValidIn(action.value, context, invokedScope);
@@ -251150,6 +252306,9 @@ ${body}
251150
252306
  );
251151
252307
  return !firedByOwnChange;
251152
252308
  }
252309
+ if (sink.type === JS_FUNCTION && c.toProperty.startsWith("in-")) {
252310
+ return component.connections.some((w) => w.toId === sink.id && w.toProperty === "run" && w.fromId !== node.id);
252311
+ }
251153
252312
  return rendered2.has(sink.id) && !isTriggerInto(sink, c.toProperty);
251154
252313
  });
251155
252314
  if (!stateWired && !actionWired && !outputRead) continue;
@@ -251301,7 +252460,8 @@ ${body}
251301
252460
  }
251302
252461
  consumed.add(wire.key);
251303
252462
  stateLandedKeys.add(wire.key);
251304
- plan.pushEffects.push({ prop: vp.prop, expr });
252463
+ const pushedType = exprTsType(expr);
252464
+ plan.pushEffects.push({ prop: vp.prop, expr, ...vp.tsType !== "any" && pushedType !== vp.tsType ? { castTo: vp.tsType } : {} });
251305
252465
  plan.liftedOutputProps.push({ port: port5, prop: vp.prop, tsType: vp.tsType });
251306
252466
  for (const k of ctx.consumes) consumed.add(k);
251307
252467
  for (const s of ctx.subscriberIds) boundSubscribers.add(s);
@@ -251565,8 +252725,60 @@ ${body}
251565
252725
  if ("defer" in r) return r;
251566
252726
  out.push(r);
251567
252727
  }
252728
+ const late2 = lateQueryFetchWrite(out);
252729
+ if (late2 !== void 0) return { defer: late2 };
251568
252730
  return out;
251569
252731
  }
252732
+ function lateQueryFetchWrite(actions) {
252733
+ const writes = (a8) => a8.kind === "store-set" ? `store:${a8.variableName}` : a8.kind === "globalstore-set" ? `key:${a8.storeName}:${a8.key}` : a8.kind === "state-set" ? `state:${a8.name}` : void 0;
252734
+ const reads = (e, into) => {
252735
+ if (e.kind === "store-get") into.add(`store:${e.variableName}`);
252736
+ else if (e.kind === "store-key-get") into.add(`key:${e.storeName}:${e.key}`);
252737
+ else if (e.kind === "state-get") into.add(`state:${e.name}`);
252738
+ else if (e.kind === "format") e.parts.forEach((p) => typeof p !== "string" && reads(p, into));
252739
+ else if (e.kind === "logical") e.operands.forEach((o) => reads(o, into));
252740
+ else if (e.kind === "not" || e.kind === "truthy") reads(e.operand, into);
252741
+ return into;
252742
+ };
252743
+ for (const [i, a8] of actions.entries()) {
252744
+ if (a8.kind !== "query-fetch") continue;
252745
+ for (const v of a8.values) {
252746
+ const read2 = reads(v.expr, /* @__PURE__ */ new Set());
252747
+ if (actions.slice(i + 1).some((b) => read2.has(writes(b) ?? ""))) {
252748
+ return `its Fetch is sent before the chain writes "${v.name}", which its Filter reads \u2014 the runtime fetches after the pulse's writes (scheduleFetch), and that order is not exported in this slice`;
252749
+ }
252750
+ }
252751
+ }
252752
+ for (const [i, a8] of actions.entries()) {
252753
+ if (!isAwaitedAction(a8)) continue;
252754
+ const sent2 = /* @__PURE__ */ new Set();
252755
+ const collect3 = (value, top) => {
252756
+ if (value === null || typeof value !== "object") return;
252757
+ if (Array.isArray(value)) return value.forEach((v) => collect3(v, false));
252758
+ const kind = value.kind;
252759
+ if (!top && typeof kind === "string" && (kind === "store-get" || kind === "store-key-get" || kind === "state-get")) {
252760
+ reads(value, sent2);
252761
+ return;
252762
+ }
252763
+ for (const [key2, child] of Object.entries(value)) if (!/Then$|^then$/.test(key2)) collect3(child, false);
252764
+ };
252765
+ collect3(a8, true);
252766
+ const laterWrites = /* @__PURE__ */ new Map();
252767
+ const writtenIn = (b) => {
252768
+ if (b === null || typeof b !== "object") return;
252769
+ if (Array.isArray(b)) return b.forEach(writtenIn);
252770
+ const key2 = writes(b);
252771
+ if (key2 !== void 0 && !laterWrites.has(key2)) laterWrites.set(key2, key2.split(":").pop() ?? key2);
252772
+ for (const child of Object.values(b)) writtenIn(child);
252773
+ };
252774
+ writtenIn(actions.slice(i + 1));
252775
+ const clash = [...sent2].find((k) => laterWrites.has(k));
252776
+ if (clash !== void 0) {
252777
+ return `its node sends "${laterWrites.get(clash)}" after the click's later wires have written it \u2014 the runtime reads what it sends after the update, the export where the call starts, and that order is not exported in this slice`;
252778
+ }
252779
+ }
252780
+ return void 0;
252781
+ }
251570
252782
  function snapAction(action, snap2) {
251571
252783
  switch (action.kind) {
251572
252784
  case "state-set": {
@@ -251719,7 +252931,9 @@ ${body}
251719
252931
  }
251720
252932
  const then2 = snapActionList(action.then, snap2);
251721
252933
  if (!Array.isArray(then2)) return then2;
251722
- return { ...action, args, then: then2 };
252934
+ const failThen = snapActionList(action.failThen, snap2);
252935
+ if (!Array.isArray(failThen)) return failThen;
252936
+ return { ...action, args, then: then2, failThen };
251723
252937
  }
251724
252938
  // Same rule for a request (EXP-011 Tier 1.2): the values it sends are read where the
251725
252939
  // handler is, before the await, so a `Set Variable` earlier in the chain must reach them —
@@ -251795,6 +253009,16 @@ ${body}
251795
253009
  }
251796
253010
  case "runtasks-abort":
251797
253011
  return action;
253012
+ // P18-S116-QUERYFETCH. A value the chain wrote a statement earlier is read as written, as `stream-action`'s.
253013
+ case "query-fetch": {
253014
+ const values = [];
253015
+ for (const v of action.values) {
253016
+ const expr = snapExpr(v.expr, snap2);
253017
+ if ("defer" in expr) return expr;
253018
+ values.push({ name: v.name, expr });
253019
+ }
253020
+ return { ...action, values };
253021
+ }
251798
253022
  case "jsfun-run": {
251799
253023
  if ((plan.jsFunctions[action.nodeId]?.inputs ?? []).some((i) => i.expr !== void 0 && exprTouchesSnap(i.expr, snap2))) {
251800
253024
  return { defer: "a Function argument reads state written earlier in this chain \u2014 not translated in this slice" };
@@ -251803,6 +253027,13 @@ ${body}
251803
253027
  if (!Array.isArray(then2)) return then2;
251804
253028
  return { ...action, then: then2 };
251805
253029
  }
253030
+ // EXP-011 §86. The arms run in the run's closure, after the body: a write earlier in the chain reaches them.
253031
+ case "jsfun-pulses":
253032
+ case "jsfun-pulse": {
253033
+ const then2 = snapActionList(action.then, snap2);
253034
+ if (!Array.isArray(then2)) return then2;
253035
+ return { ...action, then: then2 };
253036
+ }
251806
253037
  /**
251807
253038
  * EXP-011 Tier 2.5. Same rule as a request: the link is read where the handler is, before
251808
253039
  * the browser is touched, so a `Set Variable` earlier in the chain must reach it — and both
@@ -251942,7 +253173,22 @@ ${body}
251942
253173
  return action;
251943
253174
  }
251944
253175
  }
253176
+ const feedersCompiled = /* @__PURE__ */ new Set();
253177
+ const compileFunctionFeedersOf = (node) => {
253178
+ if (feedersCompiled.has(node.id)) return;
253179
+ feedersCompiled.add(node.id);
253180
+ for (const c of component.connections) {
253181
+ if (c.toId !== node.id || !c.fromProperty.startsWith("out-")) continue;
253182
+ const from = nodeById.get(c.fromId);
253183
+ if (!from || jsNodeKindOf(from.type) !== "function" || !wiredPorts.has(`${from.id}:run`)) continue;
253184
+ const runs = component.connections.filter((w) => w.toId === from.id && w.toProperty === "run");
253185
+ if (!runs.every((w) => w.fromProperty === "didMount" || rendered2.has(w.fromId))) continue;
253186
+ compileFunctionFeedersOf(from);
253187
+ compiledOf(from, "run");
253188
+ }
253189
+ };
251945
253190
  for (const node of component.nodes) {
253191
+ compileFunctionFeedersOf(node);
251946
253192
  if (TRIGGER_PORTS[node.type] !== void 0) compiledOf(node, TRIGGER_PORTS[node.type]);
251947
253193
  else if (node.type === "NavigationShowPopup") compiledOf(node, "show");
251948
253194
  else if (node.type === "NavigationClosePopup") {
@@ -252163,6 +253409,16 @@ ${body}
252163
253409
  }
252164
253410
  }
252165
253411
  const receiverActions = /* @__PURE__ */ new Map();
253412
+ const unreachedOwnerOf = (connection) => excluded.owners.get(connection.fromId) ?? (connection.fromProperty === "didMount" ? excluded.mounts.get(connection.fromId) : void 0);
253413
+ const refuseUnreached = (connection, toNode, compiled2, reason2, outputsSink) => {
253414
+ notes.push(wireNote(connection, reason2));
253415
+ for (const key2 of compiled2.consumes) consumed.add(key2);
253416
+ if (outputsSink) return;
253417
+ if (dispositions[toNode.id] === void 0) dispositions[toNode.id] = { kind: "deferred", to: "EXP-003", reason: reason2 };
253418
+ for (const id of compiled2.collapses ?? []) {
253419
+ if (dispositions[id] === void 0) dispositions[id] = { kind: "deferred", to: "EXP-003", reason: reason2 };
253420
+ }
253421
+ };
252166
253422
  for (const connection of component.connections) {
252167
253423
  if (consumed.has(connection.key)) continue;
252168
253424
  const toNode = nodeById.get(connection.toId);
@@ -252173,6 +253429,7 @@ ${body}
252173
253429
  if (fromNode?.type === "Condition" && (connection.fromProperty === "ontrue" || connection.fromProperty === "onfalse")) {
252174
253430
  continue;
252175
253431
  }
253432
+ if (pageMountFetch(connection)) continue;
252176
253433
  if ((fromNode?.type === "NavigationShowPopup" || fromNode?.type === "NavigationClosePopup") && connection.fromProperty === "done") {
252177
253434
  continue;
252178
253435
  }
@@ -252217,6 +253474,11 @@ ${body}
252217
253474
  continue;
252218
253475
  }
252219
253476
  if (fromNode && rendered2.has(fromNode.id) && isTextInputType(fromNode.type) && connection.fromProperty === "textChanged") {
253477
+ const unreached = unreachedOwnerOf(connection);
253478
+ if (unreached !== void 0) {
253479
+ refuseUnreached(connection, toNode, compiled2, unreached, outputsSink);
253480
+ continue;
253481
+ }
252220
253482
  if (!actionsValidIn([compiled2.action], { kind: "dom", nodeId: fromNode.id, port: "textChanged" })) {
252221
253483
  const reason2 = "the action reads values that only exist in another handler";
252222
253484
  if (outputsSink) {
@@ -252249,6 +253511,11 @@ ${body}
252249
253511
  const customSignal = fromNode !== void 0 && plan.roleOf[fromNode.id] === "custom" && customSignalOutputs(fromNode).has(connection.fromProperty);
252250
253512
  const repeaterRowSignal = fromNode !== void 0 && fromNode.type === "For Each" && connection.fromProperty.startsWith(ITEM_OUTPUT_SIGNAL);
252251
253513
  if (fromNode && rendered2.has(fromNode.id) && (connection.kind === "signal" || instanceSignal || customSignal || repeaterRowSignal)) {
253514
+ const unreached = unreachedOwnerOf(connection);
253515
+ if (unreached !== void 0) {
253516
+ refuseUnreached(connection, toNode, compiled2, unreached, outputsSink);
253517
+ continue;
253518
+ }
252252
253519
  if (!actionsValidIn([compiled2.action], { kind: "dom", nodeId: fromNode.id, port: connection.fromProperty })) {
252253
253520
  const reason2 = "the action reads values that only exist in another handler";
252254
253521
  if (outputsSink) {
@@ -252264,6 +253531,12 @@ ${body}
252264
253531
  compiled2.action,
252265
253532
  chainSnapshotFor(`dom:${fromNode.id}:${connection.fromProperty}`, seedId)
252266
253533
  );
253534
+ const lateWrite = "defer" in snapped ? void 0 : lateQueryFetchWrite([...plan.handlers[fromNode.id]?.[connection.fromProperty] ?? [], snapped]);
253535
+ if (lateWrite !== void 0) {
253536
+ dispositions[toNode.id] = { kind: "deferred", to: "EXP-003", reason: lateWrite };
253537
+ notes.push(wireNote(connection, lateWrite));
253538
+ continue;
253539
+ }
252267
253540
  if ("defer" in snapped) {
252268
253541
  if (outputsSink) {
252269
253542
  if (!failedOutputsNodes.has(toNode.id)) failedOutputsNodes.set(toNode.id, snapped.defer);
@@ -252300,6 +253573,12 @@ ${body}
252300
253573
  continue;
252301
253574
  }
252302
253575
  const snappedRecv = snapAction(compiled2.action, chainSnapshotFor(`recv:${fromNode.id}`));
253576
+ const lateRecvWrite = "defer" in snappedRecv ? void 0 : lateQueryFetchWrite([...receiverActions.get(fromNode.id) ?? [], snappedRecv]);
253577
+ if (lateRecvWrite !== void 0) {
253578
+ dispositions[toNode.id] = { kind: "deferred", to: "EXP-003", reason: lateRecvWrite };
253579
+ notes.push(wireNote(connection, lateRecvWrite));
253580
+ continue;
253581
+ }
252303
253582
  if ("defer" in snappedRecv) {
252304
253583
  if (outputsSink) {
252305
253584
  if (!failedOutputsNodes.has(toNode.id)) failedOutputsNodes.set(toNode.id, snappedRecv.defer);
@@ -252316,13 +253595,15 @@ ${body}
252316
253595
  for (const id of compiled2.subscribes ?? []) boundSubscribers.add(id);
252317
253596
  continue;
252318
253597
  }
252319
- const untranslatable = `trigger ${connection.fromId}.${connection.fromProperty} is not a rendered element event or a receiver`;
253598
+ const sendingRun = fromNode !== void 0 && wiredPorts.has(`${fromNode.id}:run`) ? compiledSinks.get(`${fromNode.id}:run`) : void 0;
253599
+ const pulseRefusal = fromNode !== void 0 && jsNodeKindOf(fromNode.type) === "function" ? pulseRefusals.get(`${fromNode.id}:${connection.fromProperty}`) ?? pulseRefusals.get(fromNode.id) ?? (sendingRun !== void 0 && "defer" in sendingRun && connection.fromProperty.startsWith("out-") ? `the Function that sends its "${connection.fromProperty.slice("out-".length)}" signal is not exported: ${sendingRun.defer}` : void 0) : void 0;
253600
+ const untranslatable = pulseRefusal ?? `trigger ${connection.fromId}.${connection.fromProperty} is not a rendered element event or a receiver`;
252320
253601
  if (outputsSink) {
252321
253602
  if (!failedOutputsNodes.has(toNode.id)) failedOutputsNodes.set(toNode.id, untranslatable);
252322
253603
  } else {
252323
253604
  dispositions[toNode.id] = { kind: "deferred", to: "EXP-003", reason: untranslatable };
252324
253605
  }
252325
- notes.push(wireNote(connection, `the trigger is not a rendered element event or a receiver`));
253606
+ notes.push(wireNote(connection, pulseRefusal ?? `the trigger is not a rendered element event or a receiver`));
252326
253607
  }
252327
253608
  for (const node of component.nodes) {
252328
253609
  if (node.type !== "Component Outputs" || dispositions[node.id] !== void 0) continue;
@@ -252378,6 +253659,7 @@ ${body}
252378
253659
  } else if (action.kind === "api-call") {
252379
253660
  attachedMutations.add(action.nodeId);
252380
253661
  scanActions(action.then);
253662
+ scanActions(action.failThen);
252381
253663
  } else if (action.kind === "http-call") {
252382
253664
  attachedHttpNodes.add(action.nodeId);
252383
253665
  scanActions(action.then);
@@ -252452,6 +253734,8 @@ ${body}
252452
253734
  scanActions(action.minted?.failThen ?? []);
252453
253735
  } else if (action.kind === "array-new") {
252454
253736
  scanActions(action.then);
253737
+ } else if (action.kind === "jsfun-run" || action.kind === "jsfun-pulses" || action.kind === "jsfun-pulse") {
253738
+ scanActions(action.then);
252455
253739
  }
252456
253740
  }
252457
253741
  };
@@ -252605,6 +253889,22 @@ ${body}
252605
253889
  for (const key2 of [inputs[0].key, ...ctx.consumes, ...chain.consumes]) consumed.add(key2);
252606
253890
  for (const id of [...ctx.subscriberIds, ...chain.subscribes]) boundSubscribers.add(id);
252607
253891
  }
253892
+ for (const node of component.nodes) {
253893
+ const pieces = tickedRunPieces.get(node.id);
253894
+ if (pieces === void 0 || dispositions[node.id]?.kind !== "collapsed" || !plan.file) continue;
253895
+ const snapped = snapAction(pieces.action, chainSnapshotFor(`ticked:${node.id}`));
253896
+ if ("defer" in snapped) {
253897
+ dispositions[node.id] = { kind: "deferred", to: "EXP-003", reason: `the run a ticked input starts: ${snapped.defer}` };
253898
+ notes.push(`node ${node.id} (${node.type}) deferred: the run a ticked input starts: ${snapped.defer}`);
253899
+ continue;
253900
+ }
253901
+ plan.tickedRuns.push({
253902
+ nodeId: node.id,
253903
+ watch: pieces.watch,
253904
+ action: snapped,
253905
+ comment: `${node.authoredLabel ?? node.type} \u2014 also runs when a ticked input gets a new value, sending its signals and never Done (NDA-017 \xA72).`
253906
+ });
253907
+ }
252608
253908
  for (const node of component.nodes) {
252609
253909
  if (node.type !== "Condition" || dispositions[node.id] !== void 0) continue;
252610
253910
  if (literalParam(node, "runOnChange-condition") === false) continue;
@@ -252645,6 +253945,11 @@ ${body}
252645
253945
  for (const node of component.nodes) {
252646
253946
  if (node.type !== RECORD_TYPE || dispositions[node.id] !== void 0) continue;
252647
253947
  if (wiredPorts.has(`${node.id}:fetch`)) continue;
253948
+ const refusal2 = recordBindRefusal(node);
253949
+ if (refusal2 !== void 0) {
253950
+ recordEffectDefers.set(node.id, refusal2);
253951
+ continue;
253952
+ }
252648
253953
  if (!plan.file) {
252649
253954
  recordEffectDefers.set(node.id, "component emits no file to host the effect");
252650
253955
  continue;
@@ -252666,9 +253971,13 @@ ${body}
252666
253971
  const into = `src/${plan.file.dir}/${plan.file.fileBase}.tsx`;
252667
253972
  plan.recordEffects.push({
252668
253973
  nodeId: node.id,
252669
- action: snapped.kind === "record-fetch" ? { ...snapped, viaEffect: true } : snapped,
252670
- comment: `${node.authoredLabel ?? "Record"} \u2014 read again whenever its Id changes (dbmodelnode2.ts runOnValueChange).`
253974
+ action: snapped,
253975
+ comment: `${node.authoredLabel ?? "Record"} \u2014 bound to the app's copy of the record its Id names, whenever the Id changes; binding reads nothing (dbmodelnode2.ts setModelID).`,
253976
+ held: ir.project.cloudservices !== void 0,
253977
+ follows: literalParam(node, "runOnChange-record") !== false
252671
253978
  });
253979
+ const read2 = plan.recordReads.find((r) => r.nodeId === node.id);
253980
+ if (read2 !== void 0) read2.binds = true;
252672
253981
  dispositions[node.id] = { kind: "collapsed", into };
252673
253982
  for (const id of compiled2.collapses ?? []) dispositions[id] = { kind: "collapsed", into };
252674
253983
  for (const key2 of compiled2.consumes) consumed.add(key2);
@@ -252676,6 +253985,7 @@ ${body}
252676
253985
  }
252677
253986
  for (const effect of plan.branchEffects) scanActions([effect.action]);
252678
253987
  for (const effect of plan.valueChangedEffects) scanActions(effect.actions);
253988
+ for (const ticked3 of plan.tickedRuns) scanActions([ticked3.action]);
252679
253989
  for (const effect of plan.recordEffects) scanActions([effect.action]);
252680
253990
  for (const machine of plan.statesMachines) for (const chain of statesListenerChains(machine)) scanActions(chain);
252681
253991
  for (const drag of plan.drags) for (const chain of dragListenerChains(drag)) scanActions(chain);
@@ -252783,6 +254093,10 @@ ${body}
252783
254093
  notes.push(wireNote(connection, `variable name is not a literal`));
252784
254094
  continue;
252785
254095
  }
254096
+ if (literalParam(fromNode, "runOnChange-variable") === false) {
254097
+ notes.push(wireNote(connection, VARIABLE_FROZEN));
254098
+ continue;
254099
+ }
252786
254100
  if (connection.toProperty === "mounted" && toNode.id === plan.rootId) {
252787
254101
  notes.push(wireNote(connection, `a mounted wire into the component root is a router concern \u2014 not translated in this slice`));
252788
254102
  continue;
@@ -252842,6 +254156,19 @@ ${body}
252842
254156
  const toNode = nodeById.get(connection.toId);
252843
254157
  if (!toNode || !rendered2.has(toNode.id)) continue;
252844
254158
  consumed.add(connection.key);
254159
+ const snapshotVerdict = fromNode.type === "Condition" ? snapshotVerdicts().get(fromNode.id) : void 0;
254160
+ if (snapshotVerdict === null) {
254161
+ plan.bindings[toNode.id] = plan.bindings[toNode.id] ?? {};
254162
+ plan.bindings[toNode.id][connection.toProperty] = {
254163
+ kind: "computed",
254164
+ expr: { kind: "state-get", name: snapshotCellOf(toNode.id, connection.toProperty).name }
254165
+ };
254166
+ continue;
254167
+ }
254168
+ if (snapshotVerdict !== void 0) {
254169
+ notes.push(wireNote(connection, snapshotVerdict));
254170
+ continue;
254171
+ }
252845
254172
  const ctx = newCtx();
252846
254173
  const expr = resolveExpr(fromNode, connection.fromProperty, ctx);
252847
254174
  if (expr === null) {
@@ -252925,6 +254252,12 @@ ${body}
252925
254252
  styleSinkOf(toNode, connection.toProperty) !== void 0;
252926
254253
  if (!bindable) continue;
252927
254254
  if (connection.toProperty === "mounted" && toNode.id === plan.rootId) continue;
254255
+ const deadRow = excluded.rows.get(fromNode.id);
254256
+ if (deadRow !== void 0) {
254257
+ consumed.add(connection.key);
254258
+ notes.push(wireNote(connection, deadRow));
254259
+ continue;
254260
+ }
252928
254261
  const ctx = newCtx();
252929
254262
  const expr = resolveExpr(fromNode, connection.fromProperty, ctx);
252930
254263
  if (expr === null) continue;
@@ -253136,6 +254469,12 @@ ${body}
253136
254469
  continue;
253137
254470
  }
253138
254471
  if (toNode?.type === "For Each" && connection.toProperty === "items" && fromNode?.type === "DbCollection2" && plan.repeaters[toNode.id]) {
254472
+ const settings = querySettingsFor(fromNode);
254473
+ if ("refuse" in settings) {
254474
+ consumed.add(connection.key);
254475
+ notes.push(wireNote(connection, `the Query Records it reads is not exported: ${settings.refuse}`));
254476
+ continue;
254477
+ }
253139
254478
  plan.repeaters[toNode.id].itemsQueryId = fromNode.id;
253140
254479
  consumed.add(connection.key);
253141
254480
  continue;
@@ -253454,6 +254793,7 @@ ${body}
253454
254793
  else for (const p of arg.props) walkExpr(p.expr);
253455
254794
  }
253456
254795
  walkActions(action.then);
254796
+ walkActions(action.failThen);
253457
254797
  break;
253458
254798
  // A session read is at its most ordinary inside a request — the signed-in user's id in
253459
254799
  // a path parameter, their token in a header. Missing it here would leave the page
@@ -253562,6 +254902,10 @@ ${body}
253562
254902
  break;
253563
254903
  case "runtasks-abort":
253564
254904
  break;
254905
+ // P18-S116-QUERYFETCH. The wired filter values, read at the pulse.
254906
+ case "query-fetch":
254907
+ for (const v of action.values) walkExpr(v.expr);
254908
+ break;
253565
254909
  // EXP-011 §58. The data read at the pulse; the config and listeners are walked off the plan below.
253566
254910
  case "stream-action":
253567
254911
  if (action.value !== void 0) walkExpr(action.value);
@@ -253574,6 +254918,8 @@ ${body}
253574
254918
  case "popup-show":
253575
254919
  case "popup-close":
253576
254920
  case "jsfun-run":
254921
+ case "jsfun-pulses":
254922
+ case "jsfun-pulse":
253577
254923
  // EXP-011 §30, and the same omission the sibling walker had: a session read inside an
253578
254924
  // array mutator's Done chain is as ordinary as one inside a request's, and missing it
253579
254925
  // here leaves the page importing a `useSession` the module never exported.
@@ -253603,6 +254949,11 @@ ${body}
253603
254949
  walkExpr(effect.watch);
253604
254950
  walkActions(effect.actions);
253605
254951
  }
254952
+ for (const ticked3 of plan.tickedRuns) {
254953
+ ticked3.watch.forEach(walkExpr);
254954
+ walkActions([ticked3.action]);
254955
+ }
254956
+ for (const query2 of plan.queries) for (const value of query2.wiredValues ?? []) walkExpr(value.expr);
253606
254957
  for (const machine of plan.statesMachines) {
253607
254958
  if (machine.follow !== void 0) walkExpr(machine.follow);
253608
254959
  for (const chain of statesListenerChains(machine)) walkActions(chain);
@@ -253646,6 +254997,16 @@ ${body}
253646
254997
  if (dispositions[node.id]?.kind === "collapsed") delete dispositions[node.id];
253647
254998
  }
253648
254999
  }
255000
+ for (const node of component.nodes) {
255001
+ if (node.type !== "DbCollection2" || !queryLands(node)) continue;
255002
+ for (const wire of component.connections) if (wire.toId === node.id && pageMountFetch(wire)) consumed.add(wire.key);
255003
+ const wiring = queryWirings.get(node.id);
255004
+ if (wiring === void 0) continue;
255005
+ const into = plan.file ? `src/${plan.file.dir}/${plan.file.fileBase}.tsx` : plan.path;
255006
+ for (const key2 of wiring.consumes) consumed.add(key2);
255007
+ for (const id of wiring.collapses) dispositions[id] = { kind: "collapsed", into };
255008
+ for (const id of wiring.subscribes) boundSubscribers.add(id);
255009
+ }
253649
255010
  for (const connection of component.connections) {
253650
255011
  if (consumed.has(connection.key)) continue;
253651
255012
  const kitEndpoint = undeclaredKitPortOf(connection, nodeById, kits);
@@ -253675,6 +255036,54 @@ ${body}
253675
255036
  comment: `${label2} \u2014 its authored Value is stored on every mount of this component (variablenode2.ts: the value setter runs at node creation).`
253676
255037
  });
253677
255038
  }
255039
+ {
255040
+ const parentOf = parentMapOf(component);
255041
+ for (const node of component.nodes) {
255042
+ if (!isTextInputType(node.type) || !rendered2.has(node.id)) continue;
255043
+ const stateVar = controlStateVars.get(node.id);
255044
+ const chain = plan.changeHandlers[node.id] ?? [];
255045
+ if (chain.length === 0) continue;
255046
+ const label2 = node.authoredLabel ? `"${node.authoredLabel}"` : "The text input";
255047
+ const refuse = (why) => notes.push(`node ${node.id} (${node.type}): its Value at mount is not announced \u2014 ${why}; typing still runs its chain (P18-S116-FIELDMOUNT)`);
255048
+ let away;
255049
+ for (let id = node.id; id !== void 0; id = parentOf.get(id)) {
255050
+ const bound = plan.bindings[id]?.["mounted"];
255051
+ if (bound !== void 0) {
255052
+ const literal2 = bound.kind === "computed" && (bound.expr.kind === "literal" || bound.expr.kind === "undefined");
255053
+ if (!literal2) away = away ?? "later";
255054
+ else if (bound.expr.kind === "undefined" || !bound.expr.value) away = "never";
255055
+ } else if (literalParam(nodeById.get(id), "mounted") === false) away = "never";
255056
+ }
255057
+ if (away === "never") continue;
255058
+ if (away === "later") {
255059
+ refuse("it or a group it sits in mounts from a wire, so it announces when that wire first says true, not as the component mounts");
255060
+ continue;
255061
+ }
255062
+ const type = literalParam(node, "type");
255063
+ if (wiredPorts.has(`${node.id}:type`) || type !== void 0 && typeof type !== "string") {
255064
+ refuse("its Type is wired, so whether it announces text or a number is not known here");
255065
+ continue;
255066
+ }
255067
+ if (type === "number") {
255068
+ refuse("a Number field announces a number or null, and the exported field carries text (P18-S118-NUMBERFIELD)");
255069
+ continue;
255070
+ }
255071
+ const sync = stateVar === void 0 ? void 0 : plan.syncEffects.find((e) => e.stateName === stateVar.name);
255072
+ if (sync === void 0 && wiredPorts.has(`${node.id}:startValue`) && literalParam(node, "runOnChange-startValue") !== false) {
255073
+ refuse("its Value is wired and the export does not carry that wire");
255074
+ continue;
255075
+ }
255076
+ const authored = literalParam(node, "startValue");
255077
+ const catalogDefault = node.catalogRef ? catalog2.inputDefault(node.catalogRef, "startValue") : void 0;
255078
+ const raw = authored !== void 0 ? authored : typeof catalogDefault === "string" || typeof catalogDefault === "number" ? catalogDefault : void 0;
255079
+ plan.fieldMounts.push({
255080
+ inputId: node.id,
255081
+ start: raw === void 0 ? "" : String(raw),
255082
+ ...sync !== void 0 ? { source: sync.source } : {},
255083
+ comment: `${label2} announces its Value as it mounts, and its chain runs with it (TextInput.tsx componentDidMount \u2192 setText(startValue)).`
255084
+ });
255085
+ }
255086
+ }
253678
255087
  for (const node of component.nodes) {
253679
255088
  if (node.type !== "Variable2" || dispositions[node.id] !== void 0) continue;
253680
255089
  if (variableNameOf(node) !== void 0) {
@@ -253747,9 +255156,13 @@ ${body}
253747
255156
  }
253748
255157
  for (const node of component.nodes) {
253749
255158
  if (node.type !== "DbCollection2") continue;
253750
- const consumedByRepeater = Object.values(plan.repeaters).some((r) => r.itemsQueryId === node.id);
253751
- const readByLandedTransform = queryReaders.has(node.id) && component.connections.some((c) => c.fromId === node.id && c.fromProperty === "items" && consumed.has(c.key));
253752
- if (!consumedByRepeater && !readByLandedTransform) {
255159
+ const settings = querySettingsFor(node);
255160
+ if ("refuse" in settings) {
255161
+ plan.queries = plan.queries.filter((q) => q.nodeId !== node.id);
255162
+ dispositions[node.id] = { kind: "deferred", to: "EXP-003", reason: settings.refuse };
255163
+ continue;
255164
+ }
255165
+ if (!queryLands(node)) {
253753
255166
  plan.queries = plan.queries.filter((q) => q.nodeId !== node.id);
253754
255167
  dispositions[node.id] = {
253755
255168
  kind: "deferred",
@@ -253932,6 +255345,7 @@ ${body}
253932
255345
  // EXP-011 §53. Nothing nested on the action; the listeners are filled off the plan below.
253933
255346
  case "runtasks-run":
253934
255347
  case "runtasks-abort":
255348
+ case "query-fetch":
253935
255349
  break;
253936
255350
  // EXP-011 §58. Nothing nested on the action; the listeners are filled off the plan below.
253937
255351
  case "stream-action":
@@ -254044,8 +255458,14 @@ ${body}
254044
255458
  case "popup-show":
254045
255459
  case "popup-close":
254046
255460
  case "jsfun-run":
255461
+ case "jsfun-pulses":
255462
+ case "jsfun-pulse":
255463
+ fillMaterialize(action.then);
255464
+ break;
255465
+ // EXP-011 §90. Both chains.
254047
255466
  case "api-call":
254048
255467
  fillMaterialize(action.then);
255468
+ fillMaterialize(action.failThen);
254049
255469
  break;
254050
255470
  default:
254051
255471
  break;
@@ -254057,6 +255477,7 @@ ${body}
254057
255477
  for (const receiver of plan.receivers) fillMaterialize(receiver.actions);
254058
255478
  for (const effect of plan.branchEffects) fillMaterialize([effect.action]);
254059
255479
  for (const effect of plan.valueChangedEffects) fillMaterialize(effect.actions);
255480
+ for (const ticked3 of plan.tickedRuns) fillMaterialize([ticked3.action]);
254060
255481
  for (const effect of plan.recordEffects) fillMaterialize([effect.action]);
254061
255482
  for (const machine of plan.statesMachines) for (const chain of statesListenerChains(machine)) fillMaterialize(chain);
254062
255483
  for (const drag of plan.drags) for (const chain of dragListenerChains(drag)) fillMaterialize(chain);
@@ -254069,6 +255490,22 @@ ${body}
254069
255490
  for (const stream of plan.streams) for (const chain of Object.values(stream.listeners)) fillMaterialize(chain);
254070
255491
  plan.screenResolutions = plan.screenResolutions.filter((s) => dispositions[s.nodeId]?.kind === "collapsed");
254071
255492
  }
255493
+ {
255494
+ const seen = /* @__PURE__ */ new Set();
255495
+ const visit = (value) => {
255496
+ if (value === null || typeof value !== "object" || seen.has(value)) return;
255497
+ seen.add(value);
255498
+ if (Array.isArray(value)) {
255499
+ value.forEach((item, i) => {
255500
+ if (i < value.length - 1 && isAwaitedAction(item)) item.detached = true;
255501
+ visit(item);
255502
+ });
255503
+ return;
255504
+ }
255505
+ for (const child of Object.values(value)) visit(child);
255506
+ };
255507
+ visit(plan);
255508
+ }
254072
255509
  for (const node of component.nodes) {
254073
255510
  if (node.type !== RUN_TASKS_TYPE || dispositions[node.id] !== void 0) continue;
254074
255511
  const dangling = component.connections.find((c) => c.fromId === node.id && RUN_TASKS_OUTPUTS.includes(c.fromProperty));
@@ -254712,7 +256149,7 @@ function kitFile(module2, bindings) {
254712
256149
  const prop = binding.propOf.get(input.name);
254713
256150
  if (prop === void 0) continue;
254714
256151
  if (input.displayName !== void 0) lines.push(` /** ${escapeComment(input.displayName)} */`);
254715
- lines.push(` ${prop}?: ${tsTypeOf2(input)};`);
256152
+ lines.push(` ${prop}?: ${inputTsTypeOf(input)};`);
254716
256153
  }
254717
256154
  for (const [port5, prop] of binding.signalPropOf) {
254718
256155
  lines.push(` /** Signal output "${escapeComment(port5)}". */`);
@@ -254800,6 +256237,10 @@ function tsTypeOf2(port5) {
254800
256237
  return "unknown";
254801
256238
  }
254802
256239
  }
256240
+ function inputTsTypeOf(port5) {
256241
+ const type = tsTypeOf2(port5);
256242
+ return type === "Record<string, unknown>" ? "Record<string, unknown> | readonly unknown[]" : type;
256243
+ }
254803
256244
  function runtimeFile() {
254804
256245
  return GENERATED_TS2 + `/*
254805
256246
  * The custom-node bridge (EXP-010, Route B).
@@ -265371,6 +266812,108 @@ function signInLibSource() {
265371
266812
  ].join("\n");
265372
266813
  }
265373
266814
 
266815
+ // ../nodegx-export/src/emit/functionRunLib.ts
266816
+ var FUNCTION_RUN_LIB_PATH = "src/lib/functionRun.ts";
266817
+ function functionRunLibSource() {
266818
+ return `/**
266819
+ * A Function node's run, as the running app's Function node publishes it.
266820
+ *
266821
+ * The body writes \`Outputs.x = \u2026\` and sends \`Outputs.go()\`. A write is published only when the value changes, and a
266822
+ * write of the value already held is published with the next signal. Each signal is a pulse its chain acts on after the
266823
+ * body, reading the outputs in step with the pulse: the k-th pulse reads the k-th value published since the run began,
266824
+ * or the last one when there are fewer, or what the output last sent when this run published nothing.
266825
+ */
266826
+
266827
+ /** One Function node's state, kept across runs as the running app's node keeps it. */
266828
+ export interface FunctionNode<O> {
266829
+ /** The signal names its body may send. */
266830
+ signals: readonly string[];
266831
+ /** What each output holds: the last value written to it, in any run. */
266832
+ outputs: Partial<O>;
266833
+ /** What each output last sent down its wire. \`undefined\` is never sent, so a reader keeps the value before it. */
266834
+ delivered: Partial<O>;
266835
+ }
266836
+
266837
+ /** One signal the body sent, and the outputs as that pulse's chain reads them. */
266838
+ export interface FunctionPulse<O> {
266839
+ signal: string;
266840
+ values: Partial<O>;
266841
+ }
266842
+
266843
+ export interface FunctionRun<O> {
266844
+ /** The node's outputs after the run. */
266845
+ outputs: Partial<O>;
266846
+ /** Every signal the body sent, in the order it sent them. */
266847
+ pulses: Array<FunctionPulse<O>>;
266848
+ }
266849
+
266850
+ export function functionNode<O>(signals: readonly string[]): FunctionNode<O> {
266851
+ return { signals, outputs: {}, delivered: {} };
266852
+ }
266853
+
266854
+ export function runFunction<I, O>(body: (inputs: I, outputs: O) => unknown, inputs: I, node: FunctionNode<O>): FunctionRun<O> {
266855
+ const current = node.outputs as Record<string, unknown>;
266856
+ const delivered = node.delivered as Record<string, unknown>;
266857
+ const before: Record<string, unknown> = { ...delivered };
266858
+ const published: Record<string, unknown[]> = {};
266859
+ const held = new Set<string>();
266860
+ const sent: string[] = [];
266861
+ const publish = (name: string, value: unknown) => {
266862
+ if (value === undefined) return;
266863
+ (published[name] = published[name] ?? []).push(value);
266864
+ delivered[name] = value;
266865
+ };
266866
+ const signals: Record<string, unknown> = {};
266867
+ for (const signal of node.signals) {
266868
+ const send = () => {
266869
+ held.forEach((name) => publish(name, current[name]));
266870
+ held.clear();
266871
+ sent.push(signal);
266872
+ };
266873
+ signals[signal] = Object.assign(send, { send });
266874
+ }
266875
+ const outputs = new Proxy(signals, {
266876
+ get: (target, name) => (typeof name === 'string' && name in target ? target[name] : current[name as string]),
266877
+ set: (_target, name, value) => {
266878
+ const key = String(name);
266879
+ if (value !== current[key]) {
266880
+ current[key] = value;
266881
+ held.delete(key);
266882
+ publish(key, value);
266883
+ } else {
266884
+ held.add(key);
266885
+ }
266886
+ return true;
266887
+ }
266888
+ });
266889
+ body(inputs, outputs as unknown as O);
266890
+ const count: Record<string, number> = {};
266891
+ const pulses = sent.map((signal) => {
266892
+ const k = (count[signal] = (count[signal] ?? 0) + 1);
266893
+ const values: Record<string, unknown> = { ...before };
266894
+ for (const name of Object.keys(published)) values[name] = published[name][Math.min(k, published[name].length) - 1];
266895
+ return { signal, values: values as Partial<O> };
266896
+ });
266897
+ return { outputs: node.outputs, pulses };
266898
+ }
266899
+
266900
+ /**
266901
+ * Whether the values on a node's ticked inputs (those whose Run On Value Change box is ticked) are an arrival that runs it:
266902
+ * one of them is not \`undefined\`, which is never delivered, and is not the value that input already holds. Records each
266903
+ * one that arrived. A Run records what it read the same way, so a value set and a Run sent in one event make one run.
266904
+ */
266905
+ export function tickedArrival(held: unknown[], values: readonly unknown[]): boolean {
266906
+ let arrived = false;
266907
+ values.forEach((value, i) => {
266908
+ if (value === undefined || Object.is(value, held[i])) return;
266909
+ held[i] = value;
266910
+ arrived = true;
266911
+ });
266912
+ return arrived;
266913
+ }
266914
+ `;
266915
+ }
266916
+
265374
266917
  // ../nodegx-export/src/emit/cryptoLib.ts
265375
266918
  var CRYPTO_LIB_PATH = "src/lib/crypto.ts";
265376
266919
  function cryptoLibSource() {
@@ -266404,6 +267947,33 @@ function popupDialogLibSource() {
266404
267947
  }
266405
267948
 
266406
267949
  // ../nodegx-export/src/emit/component.ts
267950
+ function queryHasEffect(query2) {
267951
+ const fetched = query2.fetchWired;
267952
+ return fetched === void 0 || fetched.mount || fetched.onValues && query2.params?.wired !== void 0;
267953
+ }
267954
+ function queryParamsLiteral(params, whereLocal) {
267955
+ const parts = [];
267956
+ if (whereLocal !== void 0) parts.push(whereLocal === "where" ? "where" : `where: ${whereLocal}`);
267957
+ else if (params.where !== void 0) parts.push(`where: ${JSON.stringify(params.where)}`);
267958
+ if (params.sort !== void 0) parts.push(`sort: [${params.sort.map((s) => tsLiteral(s)).join(", ")}]`);
267959
+ if (params.limit !== void 0) parts.push(`limit: ${params.limit}`);
267960
+ if (params.skip !== void 0) parts.push(`skip: ${params.skip}`);
267961
+ return `{ ${parts.join(", ")} }`;
267962
+ }
267963
+ function wiredWhereExpr(wired, locals, valueCode) {
267964
+ const at = (index, mask) => {
267965
+ if (index === wired.params.length) {
267966
+ const where = wired.cases[mask];
267967
+ return where === void 0 ? "undefined" : wiredWhereCode(where, valueCode);
267968
+ }
267969
+ const unset = at(index + 1, mask);
267970
+ const set2 = at(index + 1, mask | 1 << index);
267971
+ if (unset === set2) return set2;
267972
+ const arm = (code) => code.includes(" ? ") ? `(${code})` : code;
267973
+ return `${locals[index]} === undefined ? ${arm(unset)} : ${arm(set2)}`;
267974
+ };
267975
+ return at(0, 0);
267976
+ }
266407
267977
  var GENERATED_TS3 = "// @nodegx:generated (visual \u2014 provenance markers complete in EXP-007)\n";
266408
267978
  var GENERATED_CSS2 = "/* @nodegx:generated (visual) */\n";
266409
267979
  var PRINT_WIDTH = 100;
@@ -266643,6 +268213,45 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266643
268213
  const usedTimerHelpers = /* @__PURE__ */ new Set();
266644
268214
  const jsFunByNode = plan.jsFunctions;
266645
268215
  const referencedJsIds = /* @__PURE__ */ new Set();
268216
+ const pulsedRunIds = /* @__PURE__ */ new Set();
268217
+ const tickedRunIds = new Set(plan.tickedRuns.map((t) => t.nodeId));
268218
+ const tickedRecordArgs = (nodeId) => {
268219
+ const ticked3 = plan.tickedRuns.find((t) => t.nodeId === nodeId);
268220
+ return `${pulseNamesOf(jsFunByNode[nodeId]).held}, [${ticked3.watch.map((e) => exprCode(e, "handler")).join(", ")}]`;
268221
+ };
268222
+ const mountChains = [];
268223
+ const mountRefusals = /* @__PURE__ */ new Map();
268224
+ {
268225
+ const mountGateOf = (id) => {
268226
+ for (let at = id; at !== void 0; at = nodeById.get(at)?.parent) {
268227
+ const bound = plan.bindings[at]?.["mounted"];
268228
+ const literal2 = nodeById.get(at)?.parameters.find((p) => p.name === "mounted")?.value;
268229
+ const never2 = bound !== void 0 && bound.kind === "computed" && (bound.expr.kind === "undefined" || bound.expr.kind === "literal" && !bound.expr.value) || bound === void 0 && literal2?.kind === "literal" && literal2.value === false;
268230
+ const whose = at === id ? "it is" : `an element above it (${at}) is`;
268231
+ if (never2) return `${whose} never mounted, so its Did Mount never fires`;
268232
+ if (bound !== void 0 && !(bound.kind === "computed" && bound.expr.kind === "literal")) {
268233
+ return `${whose} mounted by a value, so its Did Mount fires when that value says, and a mount effect runs when the component mounts`;
268234
+ }
268235
+ if (at === plan.rootId) break;
268236
+ }
268237
+ return null;
268238
+ };
268239
+ const consider = (id) => {
268240
+ const actions = plan.handlers[id]?.["didMount"];
268241
+ if (actions === void 0) return;
268242
+ const why = mountGateOf(id);
268243
+ if (why === null) mountChains.push({ nodeId: id, actions });
268244
+ else mountRefusals.set(id, why);
268245
+ };
268246
+ const visit = (id) => {
268247
+ for (const child of plan.childrenOf[id] ?? []) visit(child);
268248
+ if (id === plan.rootId && plan.collapsedGroupId) consider(plan.collapsedGroupId);
268249
+ consider(id);
268250
+ };
268251
+ if (plan.rootId !== null) visit(plan.rootId);
268252
+ }
268253
+ const mountHandled = new Set(mountChains.map((m) => m.nodeId));
268254
+ const activePulses = /* @__PURE__ */ new Map();
266646
268255
  const referencedStateNames = /* @__PURE__ */ new Set();
266647
268256
  const stateVarByName = new Map(plan.stateVars.map((v) => [v.name, v]));
266648
268257
  const stateSetterOf = (name) => stateVarByName.get(name)?.setterName ?? `set${name}`;
@@ -266685,6 +268294,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266685
268294
  if (expr.field === "name") usedFileHelpers.add("cloudFileName");
266686
268295
  }
266687
268296
  if (expr.kind === "id-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
268297
+ if (expr.kind === "record-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
266688
268298
  if (expr.kind === "crypto-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
266689
268299
  if (expr.kind === "screen-out") usedScreenNodeIds.add(expr.nodeId);
266690
268300
  if (expr.kind === "component-object-out" && expr.parent) usedParentObject = true;
@@ -266708,6 +268318,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266708
268318
  const collectActionUse = (action) => {
266709
268319
  if (action.kind === "runtasks-run") collectExprUse(action.items);
266710
268320
  if (action.kind === "stream-action" && action.value !== void 0) collectExprUse(action.value);
268321
+ if (action.kind === "query-fetch") for (const v of action.values) collectExprUse(v.expr);
266711
268322
  if (action.kind === "state-set") {
266712
268323
  referencedStateNames.add(action.name);
266713
268324
  if (action.expr !== void 0) collectExprUse(action.expr);
@@ -266774,6 +268385,9 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266774
268385
  if (action.kind === "popup-show" || action.kind === "popup-close") {
266775
268386
  action.then.forEach(collectActionUse);
266776
268387
  }
268388
+ if (action.kind === "jsfun-pulses" || action.kind === "jsfun-pulse") {
268389
+ action.then.forEach(collectActionUse);
268390
+ }
266777
268391
  if (action.kind === "api-call") {
266778
268392
  for (const arg of action.args) {
266779
268393
  if (arg.kind === "expr") collectExprUse(arg.expr);
@@ -266783,6 +268397,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266783
268397
  }
266784
268398
  }
266785
268399
  action.then.forEach(collectActionUse);
268400
+ action.failThen.forEach(collectActionUse);
266786
268401
  }
266787
268402
  if (action.kind === "http-call" || action.kind === "cloud-call") {
266788
268403
  action.args.forEach((a8) => collectExprUse(a8.expr));
@@ -266867,6 +268482,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266867
268482
  referencedJsIds.add(action.nodeId);
266868
268483
  jsArgExprs(action.nodeId).forEach(collectExprUse);
266869
268484
  action.then.forEach(collectActionUse);
268485
+ if (runIsPulsed(action) || action.materialize !== void 0 && jsFunByNode[action.nodeId]?.pulsed === true) pulsedRunIds.add(action.nodeId);
266870
268486
  for (const name of jsFunByNode[action.nodeId]?.variables ?? []) {
266871
268487
  if (variableByName.has(name)) usedVariableNames.add(name);
266872
268488
  }
@@ -266881,14 +268497,15 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266881
268497
  ...plan.branchEffects.map((e) => e.action),
266882
268498
  // EXP-011 §39. A Value Changed's chain earns imports and references exactly as a handler's does.
266883
268499
  ...plan.valueChangedEffects.flatMap((e) => e.actions),
268500
+ // P18-S119-TICKEDRUN. The run a ticked input starts is a chain like any other.
268501
+ ...plan.tickedRuns.map((t) => t.action),
266884
268502
  // EXP-011 §49. The listeners and the arrive chain are chains like any other: a store write
266885
268503
  // inside one earns its import here, a state setter its row.
266886
268504
  ...plan.statesMachines.flatMap(
266887
268505
  (m) => [m.listeners.stateChanged, m.listeners.done, m.listeners.unchanged, m.listeners.failure, ...Object.values(m.listeners.reached)].flatMap((chain) => chain ?? [])
266888
268506
  ),
266889
268507
  ...plan.animations.flatMap((a8) => a8.arrive ?? []),
266890
- // EXP-011 §43. A Record's Id effect is a read chain run from an effect.
266891
- ...plan.recordEffects.map((e) => e.action),
268508
+ // P18-S113-IDBINDREAD. A Record's Id bind runs no chain and reads nothing: it earns only its own lines (`recordBinds`).
266892
268509
  // EXP-011 §67. A Variable's authored Value is a store write run from a mount effect — it earns its import here.
266893
268510
  ...plan.variableSeeds.map(variableSeedAction),
266894
268511
  // EXP-011 §71. An Object's own authored property values are a store write run from a mount effect — it earns its import here.
@@ -266908,36 +268525,40 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
266908
268525
  allActions.forEach(collectActionUse);
266909
268526
  for (const stream of plan.streams) if (stream.store !== void 0) usedStoreNames.add(stream.store);
266910
268527
  const deepActions = (actions) => actions.flatMap(
266911
- (a8) => a8.kind === "branch" ? [a8, ...deepActions(a8.whenTrue), ...deepActions(a8.whenFalse)] : a8.kind === "http-call" || a8.kind === "cloud-call" || a8.kind === "record-fetch" || a8.kind === "external-link" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
266912
- // EXP-011 §45. The two-arm files actions ride the line above's shape; the picker has a third arm.
266913
- a8.kind === "file-upload" || a8.kind === "file-sign" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : a8.kind === "file-pick" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...deepActions(a8.failThen)] : (
266914
- // EXP-011 §15. `Navigate To Path` carries two chains and neither is `failThen`;
266915
- // without this line `usesNavigate` below cannot see a second navigation nested in
266916
- // the first one's Done, and the `useNavigate()` hook it needs goes undeclared.
266917
- a8.kind === "navigate-path" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen), ...deepActions(a8.completedThen)] : (
266918
- // EXP-011 §37. Two arms, so it cannot join the single-chain list below — a popup
266919
- // opened from a UUID's Failure chain is a popup nothing here knows is attached.
266920
- a8.kind === "id-new" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
266921
- // EXP-011 §76. Four arms: the extractor's two result chains print inside the Done arm.
266922
- a8.kind === "crypto-call" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen), ...deepActions(a8.foundThen), ...deepActions(a8.notFoundThen)] : (
266923
- // EXP-011 §60. Three chains — a popup opened from a record write's Done, or a raise in a parent write's
266924
- // Failure, is nowhere without this line.
266925
- a8.kind === "component-object-set" ? [a8, ...deepActions(a8.then), ...deepActions(a8.completedThen), ...deepActions(a8.failThen)] : (
266926
- // EXP-011 §61. The pusher's three callback families; the Pop's two arms.
266927
- a8.kind === "stack-push" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...a8.backActions.flatMap((b) => deepActions(b.then))] : a8.kind === "stack-pop" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
266928
- // EXP-011 §39. Four chains — a popup opened from a Delay's Finished is the shape
266929
- // this node exists for, and it is nowhere without this line.
266930
- a8.kind === "delay" ? [
266931
- a8,
266932
- ...deepActions(a8.then),
266933
- ...deepActions(a8.unchangedThen),
266934
- ...deepActions(a8.startedThen),
266935
- ...deepActions(a8.finishedThen)
266936
- ] : (
266937
- // EXP-011 §55. The array mutators own chains too (§30.3's walker had the same hole): a Clear's
266938
- // two outcome arms and its minted Failure arm, an Insert's minted Failure arm, a Remove's Done.
266939
- a8.kind === "collection-clear" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...deepActions(a8.minted?.failThen ?? [])] : a8.kind === "collection-add" ? [a8, ...deepActions(a8.minted?.failThen ?? [])] : a8.kind === "log" || a8.kind === "popup-show" || a8.kind === "popup-close" || a8.kind === "jsfun-run" || a8.kind === "api-call" || a8.kind === "date-now-read" || a8.kind === "collection-remove" || a8.kind === "array-new" || // EXP-011 §61. A Reset's one chain.
266940
- a8.kind === "stack-reset" ? [a8, ...deepActions(a8.then)] : [a8]
268528
+ (a8) => a8.kind === "branch" ? [a8, ...deepActions(a8.whenTrue), ...deepActions(a8.whenFalse)] : (
268529
+ // EXP-011 §90. A record or user verb's Failure chain rides the same line.
268530
+ a8.kind === "api-call" || a8.kind === "http-call" || a8.kind === "cloud-call" || a8.kind === "record-fetch" || a8.kind === "external-link" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
268531
+ // EXP-011 §45. The two-arm files actions ride the line above's shape; the picker has a third arm.
268532
+ a8.kind === "file-upload" || a8.kind === "file-sign" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : a8.kind === "file-pick" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...deepActions(a8.failThen)] : (
268533
+ // EXP-011 §15. `Navigate To Path` carries two chains and neither is `failThen`;
268534
+ // without this line `usesNavigate` below cannot see a second navigation nested in
268535
+ // the first one's Done, and the `useNavigate()` hook it needs goes undeclared.
268536
+ a8.kind === "navigate-path" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen), ...deepActions(a8.completedThen)] : (
268537
+ // EXP-011 §37. Two arms, so it cannot join the single-chain list below — a popup
268538
+ // opened from a UUID's Failure chain is a popup nothing here knows is attached.
268539
+ a8.kind === "id-new" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
268540
+ // EXP-011 §76. Four arms: the extractor's two result chains print inside the Done arm.
268541
+ a8.kind === "crypto-call" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen), ...deepActions(a8.foundThen), ...deepActions(a8.notFoundThen)] : (
268542
+ // EXP-011 §60. Three chains — a popup opened from a record write's Done, or a raise in a parent write's
268543
+ // Failure, is nowhere without this line.
268544
+ a8.kind === "component-object-set" ? [a8, ...deepActions(a8.then), ...deepActions(a8.completedThen), ...deepActions(a8.failThen)] : (
268545
+ // EXP-011 §61. The pusher's three callback families; the Pop's two arms.
268546
+ a8.kind === "stack-push" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...a8.backActions.flatMap((b) => deepActions(b.then))] : a8.kind === "stack-pop" ? [a8, ...deepActions(a8.then), ...deepActions(a8.failThen)] : (
268547
+ // EXP-011 §39. Four chains — a popup opened from a Delay's Finished is the shape
268548
+ // this node exists for, and it is nowhere without this line.
268549
+ a8.kind === "delay" ? [
268550
+ a8,
268551
+ ...deepActions(a8.then),
268552
+ ...deepActions(a8.unchangedThen),
268553
+ ...deepActions(a8.startedThen),
268554
+ ...deepActions(a8.finishedThen)
268555
+ ] : (
268556
+ // EXP-011 §55. The array mutators own chains too (§30.3's walker had the same hole): a Clear's
268557
+ // two outcome arms and its minted Failure arm, an Insert's minted Failure arm, a Remove's Done.
268558
+ a8.kind === "collection-clear" ? [a8, ...deepActions(a8.then), ...deepActions(a8.unchangedThen), ...deepActions(a8.minted?.failThen ?? [])] : a8.kind === "collection-add" ? [a8, ...deepActions(a8.minted?.failThen ?? [])] : a8.kind === "log" || a8.kind === "popup-show" || a8.kind === "popup-close" || a8.kind === "jsfun-run" || // EXP-011 §86. The pulse loop and each signal's arm.
268559
+ a8.kind === "jsfun-pulses" || a8.kind === "jsfun-pulse" || a8.kind === "date-now-read" || a8.kind === "collection-remove" || a8.kind === "array-new" || // EXP-011 §61. A Reset's one chain.
268560
+ a8.kind === "stack-reset" ? [a8, ...deepActions(a8.then)] : [a8]
268561
+ )
266941
268562
  )
266942
268563
  )
266943
268564
  )
@@ -267005,6 +268626,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267005
268626
  }
267006
268627
  if (expr.kind === "now-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
267007
268628
  if (expr.kind === "id-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
268629
+ if (expr.kind === "record-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
267008
268630
  if (expr.kind === "crypto-out" && expr.viaState !== void 0) referencedStateNames.add(expr.viaState);
267009
268631
  if (expr.kind === "screen-out") usedScreenNodeIds.add(expr.nodeId);
267010
268632
  if (expr.kind === "component-object-out" && expr.parent) usedParentObject = true;
@@ -267051,21 +268673,23 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267051
268673
  if (effect.action.kind === "branch") hookExprSources(effect.action.cond);
267052
268674
  }
267053
268675
  for (const effect of plan.valueChangedEffects) hookExprSources(effect.watch);
268676
+ for (const ticked3 of plan.tickedRuns) ticked3.watch.forEach(hookExprSources);
267054
268677
  for (const animation of plan.animations) {
267055
268678
  hookExprSources(animation.target);
267056
268679
  hookExprSources(animation.duration);
267057
268680
  hookExprSources(animation.delay);
267058
268681
  }
267059
268682
  for (const machine of plan.statesMachines) if (machine.follow !== void 0) hookExprSources(machine.follow);
268683
+ for (const query2 of plan.queries) {
268684
+ if (!queryHasEffect(query2)) continue;
268685
+ for (const value of query2.wiredValues ?? []) hookExprSources(value.expr);
268686
+ }
267060
268687
  for (const drag of plan.drags) for (const option of drag.options) hookExprSources(option.expr);
267061
268688
  for (const stream of plan.streams) for (const entry of stream.config) hookExprSources(entry.expr);
267062
268689
  for (const run6 of plan.runTasks) {
267063
268690
  hookExprSources(run6.maxRunningTasks);
267064
268691
  hookExprSources(run6.stopOnFailure);
267065
268692
  }
267066
- for (const effect of plan.recordEffects) {
267067
- if (effect.action.kind === "record-fetch") hookExprSources(effect.action.id);
267068
- }
267069
268693
  for (const stateVar of plan.stateVars) {
267070
268694
  if (stateVar.origin === "control" && plan.roleOf[stateVar.originNodeId] !== void 0) {
267071
268695
  referencedStateNames.add(stateVar.name);
@@ -267096,7 +268720,19 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267096
268720
  }
267097
268721
  }
267098
268722
  const referencedStateVars = plan.stateVars.filter((v) => referencedStateNames.has(v.name));
267099
- const usesNavigate = deepActions(allActions).some((a8) => a8.kind === "navigate" || a8.kind === "navigate-path");
268723
+ const liveRecordRows = plan.recordReads.flatMap((read2) => {
268724
+ const row = read2.live && !read2.binds ? referencedStateVars.find((v) => v.originNodeId === read2.nodeId && v.origin === "record-row") : void 0;
268725
+ return row === void 0 ? [] : [{ read: read2, row }];
268726
+ });
268727
+ const recordBinds = plan.recordEffects.flatMap((effect) => {
268728
+ const action = effect.action;
268729
+ if (action.kind !== "record-fetch" || !effect.held) return [];
268730
+ const row = referencedStateVars.find((v) => v.originNodeId === effect.nodeId && v.origin === "record-row");
268731
+ const read2 = plan.recordReads.find((r) => r.nodeId === effect.nodeId);
268732
+ return row === void 0 || read2 === void 0 ? [] : [{ effect, action, read: read2, row }];
268733
+ });
268734
+ for (const { action } of recordBinds) hookExprSources(action.id);
268735
+ const usesNavigate = deepActions(allActions).some((a8) => a8.kind === "navigate" && !a8.newTab || a8.kind === "navigate-path");
267100
268736
  const raisesAppErrors = deepActions(allActions).some((a8) => RAISING_ACTION_KINDS.has(a8.kind) || mintedGuards(a8) || a8.kind === "component-object-set" && a8.parent);
267101
268737
  const reserved = /* @__PURE__ */ new Set([
267102
268738
  "event",
@@ -267149,7 +268785,11 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267149
268785
  const deferrals = deferralsByNode.get(nodeId);
267150
268786
  if (deferrals === void 0) return [];
267151
268787
  flushed3.add(nodeId);
267152
- const out = [`TODO(export): node ${commentSafe(nodeId)} renders, and part of what the graph`, "asked of it did not survive:"];
268788
+ const undrawn = plan.roleOf[nodeId] !== void 0 ? undrawnReasonOf(plan, component, nodeId) : null;
268789
+ const out = [
268790
+ undrawn === null ? `TODO(export): node ${commentSafe(nodeId)} renders, and part of what the graph` : `TODO(export): node ${commentSafe(nodeId)} is not drawn (${commentSafe(undrawn)}), and part of what the graph`,
268791
+ "asked of it did not survive:"
268792
+ ];
267153
268793
  for (const d of deferrals) {
267154
268794
  out.push(` - ${commentSafe(d.subject)} ${commentSafe(d.reason)}.`);
267155
268795
  if (d.origin !== null) out.push(` In the graph it is fed by ${commentSafe(d.origin)}.`);
@@ -267440,6 +269080,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267440
269080
  return SIMPLE_REF.test(code) || /^[A-Za-z_$][\w$.]*\(\)$/.test(code) || code.endsWith(")") ? code : `(${code})`;
267441
269081
  };
267442
269082
  const jsArgsObject = (def, mode) => {
269083
+ if (def.kind === "expression" && def.inputs.length === 0) return "";
267443
269084
  const entries2 = def.inputs.filter((i) => i.expr !== void 0).map((i) => {
267444
269085
  const code = exprCode(i.expr, mode);
267445
269086
  const key3 = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(i.name) ? i.name : JSON.stringify(i.name);
@@ -267478,12 +269119,13 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267478
269119
  if (node === "pattern") return `${local}.${field2 ?? "match"}`;
267479
269120
  return `${local}.${node === "hash" ? "digest" : "value"}`;
267480
269121
  };
269122
+ let announcedCode;
267481
269123
  const exprCode = (expr, mode) => {
267482
269124
  switch (expr.kind) {
267483
269125
  case "prop":
267484
269126
  return propName(expr.name);
267485
269127
  case "input-text":
267486
- return "event.target.value";
269128
+ return announcedCode?.inputId === expr.inputId ? announcedCode.code : "event.target.value";
267487
269129
  /**
267488
269130
  * A url parameter (EXP-011 Tier 2.5) — the query first, the path segment behind it.
267489
269131
  *
@@ -267789,15 +269431,24 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267789
269431
  // Expression's typed getters, verbatim semantics (expression.ts).
267790
269432
  case "jsfun-out": {
267791
269433
  const def = jsFunByNode[expr.nodeId];
267792
- if (expr.viaState !== void 0 && mode === "render") {
269434
+ if (expr.viaState !== void 0 && mode === "handler" && def.kind === "function" && pulsedRunIds.has(expr.nodeId)) {
269435
+ const delivered = memberExpr(`${pulseNamesOf(def).node}.delivered`, expr.output);
269436
+ if (expr.fold === "string") return `String(${delivered} ?? '')`;
269437
+ if (expr.fold === "number") return `(typeof ${delivered} === 'number' ? ${delivered} : Number(${delivered}) || 0)`;
269438
+ if (expr.fold === "boolean") return `!!${delivered}`;
269439
+ return delivered;
269440
+ }
269441
+ if (expr.viaState !== void 0) {
267793
269442
  const base2 = def.kind !== "expression" ? /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(expr.output) ? `${expr.viaState}?.${expr.output}` : `${expr.viaState}?.[${JSON.stringify(expr.output)}]` : expr.viaState;
267794
269443
  if (expr.fold === "string") return `String(${base2} ?? '')`;
267795
269444
  if (expr.fold === "number") return `(typeof ${base2} === 'number' ? ${base2} : Number(${base2}) || 0)`;
267796
269445
  if (expr.fold === "boolean") return `!!${base2}`;
267797
269446
  return base2;
267798
269447
  }
269448
+ const pulse = mode === "handler" ? activePulses.get(expr.nodeId) : void 0;
269449
+ const ran2 = mode === "handler" && pulse === void 0 && pulsedRunIds.has(expr.nodeId) ? `${pulseNamesOf(def).run}.outputs` : void 0;
267799
269450
  const local = mode === "render" ? jsLocals.get(expr.nodeId) : void 0;
267800
- const callee = local ?? `${def.fnName}(${jsArgsObject(def, mode)})`;
269451
+ const callee = pulse !== void 0 ? `${pulse}.values` : ran2 ?? local ?? `${def.fnName}(${jsArgsObject(def, mode)})`;
267801
269452
  const base = def.kind === "function" ? memberExpr(callee, expr.output) : callee;
267802
269453
  if (expr.fold === "string") return `String(${base} ?? '')`;
267803
269454
  if (expr.fold === "number") return `(typeof ${base} === 'number' ? ${base} : Number(${base}) || 0)`;
@@ -267914,7 +269565,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267914
269565
  break;
267915
269566
  case "jsfun-out": {
267916
269567
  if (e.viaState !== void 0) {
267917
- add(e.viaState);
269568
+ add(exprCode({ ...e, fold: void 0 }, "render"));
267918
269569
  break;
267919
269570
  }
267920
269571
  const local = jsLocals.get(e.nodeId);
@@ -267951,7 +269602,14 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
267951
269602
  // A jsfun-run is only its done-chain (EXP-003 §4 A2h): output reads inline the call
267952
269603
  // at their sinks, so the run itself needs no statement — unless it materializes its
267953
269604
  // output record (CONTROLLED-STATE §4f), which is one setter statement before the chain.
267954
- a8.kind === "jsfun-run" ? a8.materialize !== void 0 ? [{ ...a8, then: [] }, ...expandActions(a8.then)] : expandActions(a8.then) : [a8]
269605
+ a8.kind === "jsfun-run" ? a8.materialize !== void 0 || runIsPulsed(a8) ? [{ ...a8, then: [] }, ...expandActions(a8.then)] : expandActions(a8.then) : (
269606
+ // P18-S116-QUERYFETCH. A Fetch into a query the export does not keep (nothing it renders reads the rows) asks
269607
+ // for nothing anyone sees; the query's own row in the report says why it is not kept.
269608
+ a8.kind === "query-fetch" && !plan.queries.some((q) => q.nodeId === a8.nodeId) ? [] : (
269609
+ // EXP-011 §87. A literal Condition's one side is following statements.
269610
+ a8.kind === "branch" && a8.flat === true ? expandActions(a8.whenTrue) : [a8]
269611
+ )
269612
+ )
267955
269613
  )
267956
269614
  )
267957
269615
  )
@@ -268011,7 +269669,7 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
268011
269669
  case "branch":
268012
269670
  return reads(a8.cond) || a8.whenTrue.some(inAction) || a8.whenFalse.some(inAction);
268013
269671
  case "api-call":
268014
- return a8.args.some((arg) => arg.kind === "expr" ? reads(arg.expr) : arg.props.some((p) => reads(p.expr))) || a8.then.some(inAction);
269672
+ return a8.args.some((arg) => arg.kind === "expr" ? reads(arg.expr) : arg.props.some((p) => reads(p.expr))) || a8.then.some(inAction) || a8.failThen.some(inAction);
268015
269673
  case "http-call":
268016
269674
  // EXP-011 §41. The same, one node over.
268017
269675
  case "cloud-call":
@@ -268062,12 +269720,17 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
268062
269720
  return reads(a8.items);
268063
269721
  case "runtasks-abort":
268064
269722
  return false;
269723
+ // P18-S116-QUERYFETCH. The wired filter values are read at the pulse.
269724
+ case "query-fetch":
269725
+ return a8.values.some((v) => reads(v.expr));
268065
269726
  // EXP-011 §58. The data is read at the pulse.
268066
269727
  case "stream-action":
268067
269728
  return a8.value !== void 0 && reads(a8.value);
268068
269729
  case "popup-show":
268069
269730
  case "popup-close":
268070
269731
  case "jsfun-run":
269732
+ case "jsfun-pulses":
269733
+ case "jsfun-pulse":
268071
269734
  case "date-now-read":
268072
269735
  return a8.then.some(inAction);
268073
269736
  // EXP-011 §37. Both arms, on the hazard the two notes above name: the Failure arm can
@@ -268120,6 +269783,11 @@ function emitComponent(plan, project2, ir, catalog2, kitBindings = /* @__PURE__
268120
269783
  return [`if (${m.stateName} === null) {`, ...guard, `${at}} else {`, ...elseLines.map((l) => `${inner}${l}`), `${at}}`].join("\n");
268121
269784
  };
268122
269785
  const actionCode = (action, indent = 0) => {
269786
+ if ("detached" in action && action.detached === true) {
269787
+ return `void (async () => {
269788
+ ${pad(indent + 2)}${actionCode({ ...action, detached: void 0 }, indent + 2)}
269789
+ ${pad(indent)}})()`;
269790
+ }
268123
269791
  switch (action.kind) {
268124
269792
  /**
268125
269793
  * The url the runtime's `getRelativeURL` would build, built here instead.
@@ -268246,7 +269914,8 @@ ${at}`);
268246
269914
  );
268247
269915
  const query2 = action.query.map((param) => `${encodeURIComponent(param.name)}=\${${urlValue(param.expr)}}`).join("&");
268248
269916
  const url = query2 === "" ? filled : `${filled}?${query2}`;
268249
- return action.pathParams.length === 0 && action.query.length === 0 ? `navigate('${url}')` : `navigate(\`${url}\`)`;
269917
+ const literal2 = action.pathParams.length === 0 && action.query.length === 0 ? `'${url}'` : `\`${url}\``;
269918
+ return action.newTab ? `window.open(${literal2}, '_blank')` : `navigate(${literal2})`;
268250
269919
  }
268251
269920
  // The optional call is the runtime's hasOutput/unwired case: a parent that passes
268252
269921
  // nothing gets nothing (COMPONENT-OUTPUTS-TARGET §3).
@@ -268400,16 +270069,63 @@ ${at}`);
268400
270069
  return `${action.local}.run(${exprCode(action.items, "handler")})`;
268401
270070
  case "runtasks-abort":
268402
270071
  return `${action.local}.abort()`;
270072
+ /**
270073
+ * P18-S116-QUERYFETCH. A Query Records' Fetch: the query's own settings, a wired filter's `where` from the values as
270074
+ * the handler reads them at the pulse (a store fresh, `.get()`). Not awaited: the runtime schedules its fetch.
270075
+ */
270076
+ case "query-fetch": {
270077
+ const query2 = plan.queries.find((q) => q.nodeId === action.nodeId);
270078
+ const wired = query2.params?.wired;
270079
+ if (query2.params === void 0) return `void ${query2.fetchName}().then(${query2.setterName})`;
270080
+ if (wired === void 0) return `void ${query2.fetchName}(${queryParamsLiteral(query2.params)}).then(${query2.setterName})`;
270081
+ const codes = action.values.map((v) => exprCode(v.expr, "handler"));
270082
+ const codeOf = (name) => codes[action.values.findIndex((v) => v.name === name)];
270083
+ const tested = codes.map((code) => SIMPLE_REF.test(code) ? code : `(${code})`);
270084
+ const valueCode = (name, date3) => query2.wiredClient ? `filterValue(${codeOf(name)}${date3 ? ", true" : ""})` : codeOf(name);
270085
+ return `void ${query2.fetchName}(${queryParamsLiteral(query2.params, wiredWhereExpr(wired, tested, valueCode))}).then(${query2.setterName})`;
270086
+ }
268403
270087
  /** A streaming node's action (EXP-011 §58) — one call on the handle, the data read at the pulse where the verb takes it. */
268404
270088
  case "stream-action":
268405
270089
  return `${action.local}.${action.verb}(${action.value !== void 0 ? exprCode(action.value, "handler") : ""})`;
268406
270090
  case "jsfun-run": {
270091
+ const def = jsFunByNode[action.nodeId];
270092
+ const record2 = action.ticked !== true && tickedRunIds.has(action.nodeId) ? `tickedArrival(${tickedRecordArgs(action.nodeId)});
270093
+ ${pad(indent)}` : "";
270094
+ if (pulsedRunIds.has(action.nodeId) && action.then.length === 0) {
270095
+ const names2 = pulseNamesOf(def);
270096
+ const run6 = `${record2}const ${names2.run} = runFunction(${def.fnName}, ${jsArgsObject(def, "handler")}, ${names2.node})`;
270097
+ if (action.materialize === void 0) return run6;
270098
+ return `${run6};
270099
+ ${pad(indent)}${stateSetterOf(action.materialize)}({ ...${names2.run}.outputs })`;
270100
+ }
268407
270101
  if (action.materialize !== void 0) {
268408
- const def = jsFunByNode[action.nodeId];
268409
- return `${stateSetterOf(action.materialize)}(${def.fnName}(${jsArgsObject(def, "handler")}))`;
270102
+ return `${record2}${stateSetterOf(action.materialize)}(${def.fnName}(${jsArgsObject(def, "handler")}))`;
268410
270103
  }
268411
270104
  return expandActions(action.then).map(actionCode).join("; ");
268412
270105
  }
270106
+ /**
270107
+ * EXP-011 §86. One loop over the pulses the run sent, in the order it sent them; each arm is one signal's chain,
270108
+ * and inside it this Function's outputs read as that pulse's targets read them (`exprCode`'s `jsfun-out`).
270109
+ */
270110
+ case "jsfun-pulses": {
270111
+ const names2 = pulseNamesOf(jsFunByNode[action.nodeId]);
270112
+ const arms = action.then.filter((a8) => a8.kind === "jsfun-pulse");
270113
+ activePulses.set(action.nodeId, names2.pulse);
270114
+ try {
270115
+ const lines = [`for (const ${names2.pulse} of ${names2.run}.pulses) {`];
270116
+ arms.forEach((arm, i) => {
270117
+ const head = `${i === 0 ? "" : "} else "}if (${names2.pulse}.signal === ${tsLiteral(arm.signal)}) {`;
270118
+ lines.push(`${pad(indent + 2)}${head}`, ...blockBody(expandActions(arm.then), indent + 4));
270119
+ });
270120
+ lines.push(`${pad(indent + 2)}}`, `${pad(indent)}}`);
270121
+ return lines.join("\n");
270122
+ } finally {
270123
+ activePulses.delete(action.nodeId);
270124
+ }
270125
+ }
270126
+ // Printed by its loop above; never on its own.
270127
+ case "jsfun-pulse":
270128
+ return expandActions(action.then).map((a8) => actionCode(a8, indent)).join("; ");
268413
270129
  // The functional updates are §3.3's closure-staleness-immune forms.
268414
270130
  case "state-set": {
268415
270131
  const setter = stateSetterOf(action.name);
@@ -268428,7 +270144,11 @@ ${at}`);
268428
270144
  );
268429
270145
  if (action.coerceId === "string") args[0] = `String(${args[0]} ?? '')`;
268430
270146
  const inner = pad(indent + 2);
270147
+ const boundId = action.guardId && action.coerceId !== "string" && args[0].endsWith(")") ? `${action.errorState.replace(/Error$/, "")}RecordId` : void 0;
270148
+ const idRead = args[0];
270149
+ if (boundId !== void 0) args[0] = boundId;
268431
270150
  const body2 = [
270151
+ ...boundId !== void 0 ? [`${inner}const ${boundId} = ${idRead};`] : [],
268432
270152
  // The runtime's "Missing Record Id" (RECORD-VERBS-TARGET §1), thrown rather than
268433
270153
  // branched so it lands in the catch below — which is already the graph's own error
268434
270154
  // path — skips the call and the done chain, and narrows the id for the call itself.
@@ -268461,6 +270181,8 @@ ${at}`);
268461
270181
  `${inner}${stateSetterOf(action.errorState)}(${action.errorState}Message);`,
268462
270182
  // EXP-011 §54. The Error row first, then the raise, then nothing — the order the setError funnels keep.
268463
270183
  `${inner}${raiseLine(action.nodeId, errorCodeOf(action), `${action.errorState}Message`)}`,
270184
+ // EXP-011 §90. Then the Failure pulse's chain: `reportOutcome` raises before it sends the signal.
270185
+ ...expandActions(action.failThen).map((a8) => `${inner}${actionCode(a8, indent + 2)};`),
268464
270186
  `${pad(indent)}}`
268465
270187
  ].join("\n");
268466
270188
  }
@@ -268507,9 +270229,10 @@ ${at}`);
268507
270229
  /**
268508
270230
  * A `Record`'s Fetch (EXP-011 §43). One argument, the Id; the runtime's own `Missing Id.`
268509
270231
  * thrown inside the try where the Id is empty — `scheduleFetch` reports it as Failure before
268510
- * any request, so the catch is exactly the arm it belongs in. The row is replaced, not
268511
- * merged: `setModelID` binds a fresh model per Id, so a column the answer lacks reads
268512
- * undefined, as the node's output does for a record that never carried it.
270232
+ * any request, so the catch is exactly the arm it belongs in. The row is replaced by what
270233
+ * the read answers. Where the app has a live Record (P18-S111-RECORDLIVE) that answer is the
270234
+ * client's one copy of the record, the read merged into it field by field as `_fromJSON`
270235
+ * merges into `Model.get(id)`; otherwise it is the wire's answer alone.
268513
270236
  */
268514
270237
  case "record-fetch": {
268515
270238
  const names2 = recordNamesOf(action.nodeId);
@@ -268517,11 +270240,9 @@ ${at}`);
268517
270240
  const idCode = action.coerceId === "string" ? `String(${exprCode(action.id, "handler")} ?? '')` : exprCode(action.id, "handler");
268518
270241
  const idLocal = names2.idLocal;
268519
270242
  const empty3 = `${idLocal} === undefined || ${idLocal} === null || ${idLocal} === ''`;
268520
- const guard = action.id.kind === "literal" ? [] : action.viaEffect ? [`${inner}if (${empty3}) return;`] : [`${inner}if (${empty3}) throw new Error('Missing Id.');`];
268521
- const clear2 = action.viaEffect && action.materialize !== void 0 ? [`${inner}${stateSetterOf(action.materialize)}(undefined);`] : [];
270243
+ const guard = action.id.kind === "literal" ? [] : [`${inner}if (${empty3}) throw new Error('Missing Id.');`];
268522
270244
  return [
268523
270245
  "try {",
268524
- ...clear2,
268525
270246
  `${inner}const ${idLocal} = ${idCode};`,
268526
270247
  ...guard,
268527
270248
  `${inner}const ${names2.answerLocal} = await ${action.fnName}(${idLocal});`,
@@ -269044,7 +270765,9 @@ ${pad(indent)}}`;
269044
270765
  }
269045
270766
  };
269046
270767
  function actionIsStatement(a8) {
269047
- return a8.kind === "branch" || a8.kind === "popup-close" && a8.then.length > 0 || a8.kind === "collection-clear" && (a8.then.length > 0 || a8.unchangedThen.length > 0) || // EXP-011 §55. The mint is statements; a guarded mutator is an `if`/`else`.
270768
+ return a8.kind === "branch" || // EXP-011 §86. A run with pulses declares its `const`, and its loop is a `for`.
270769
+ a8.kind === "jsfun-run" && pulsedRunIds.has(a8.nodeId) || // P18-S119-TICKEDRUN. A Run that records what it read is two statements: the record, then the run. The sixth instance.
270770
+ a8.kind === "jsfun-run" && a8.ticked !== true && tickedRunIds.has(a8.nodeId) || a8.kind === "jsfun-pulses" || a8.kind === "popup-close" && a8.then.length > 0 || a8.kind === "collection-clear" && (a8.then.length > 0 || a8.unchangedThen.length > 0) || // EXP-011 §55. The mint is statements; a guarded mutator is an `if`/`else`.
269048
270771
  a8.kind === "array-new" || mintedGuards(a8) || /**
269049
270772
  * 🔴 `Now`'s Read declares a `const`, and a `const` is a statement (EXP-011 Tier 1.3).
269050
270773
  * `() => const clockRead = new Date(); setClock(clockRead)` does not parse — the fourth
@@ -269086,17 +270809,21 @@ ${pad(indent)}}`;
269086
270809
  a8.kind === "stack-push" || a8.kind === "stack-pop" || a8.kind === "stack-reset" && a8.then.length > 0;
269087
270810
  }
269088
270811
  function actionTakesNoTerminator(a8) {
269089
- return a8.kind === "api-call" || a8.kind === "http-call" || // EXP-011 §41. A try/catch, like the two above.
269090
- a8.kind === "cloud-call" || // EXP-011 §43. A try/catch, like the three above.
269091
- a8.kind === "record-fetch" || // EXP-011 §45. Three more try/catches.
269092
- a8.kind === "file-pick" || a8.kind === "file-upload" || a8.kind === "file-sign" || a8.kind === "external-link" && externalLinkIsStatement(a8) || a8.kind === "navigate-path" && navigatePathIsStatement(a8) || // EXP-011 §39. The `if` form ends in `}` and must not take a terminator.
269093
- a8.kind === "delay" && delayIsBlock(a8) || // EXP-011 §55. The guarded mutator ends in `}` too.
269094
- mintedGuards(a8) || // EXP-011 §37. The block form ends in `}` and must not take a terminator; the
269095
- // `Unique Id` form is a run of statements and takes one, exactly as a Now Read does.
269096
- a8.kind === "id-new" && idNewIsBlock(a8) || // EXP-011 §59. The block form ends in `}` too.
269097
- a8.kind === "crypto-call" || // EXP-011 §60. The parent write's `if` ends in `}` and takes no terminator.
269098
- a8.kind === "component-object-set" && a8.parent || // EXP-011 §61. The pop's block form ends in `}`; the push ends in `)` and takes its `;`, as does a Reset.
269099
- a8.kind === "stack-pop";
270812
+ if ("detached" in a8 && a8.detached === true) return false;
270813
+ return (
270814
+ // EXP-011 §86. The pulse loop ends in `}`.
270815
+ a8.kind === "jsfun-pulses" || a8.kind === "api-call" || a8.kind === "http-call" || // EXP-011 §41. A try/catch, like the two above.
270816
+ a8.kind === "cloud-call" || // EXP-011 §43. A try/catch, like the three above.
270817
+ a8.kind === "record-fetch" || // EXP-011 §45. Three more try/catches.
270818
+ a8.kind === "file-pick" || a8.kind === "file-upload" || a8.kind === "file-sign" || a8.kind === "external-link" && externalLinkIsStatement(a8) || a8.kind === "navigate-path" && navigatePathIsStatement(a8) || // EXP-011 §39. The `if` form ends in `}` and must not take a terminator.
270819
+ a8.kind === "delay" && delayIsBlock(a8) || // EXP-011 §55. The guarded mutator ends in `}` too.
270820
+ mintedGuards(a8) || // EXP-011 §37. The block form ends in `}` and must not take a terminator; the
270821
+ // `Unique Id` form is a run of statements and takes one, exactly as a Now Read does.
270822
+ a8.kind === "id-new" && idNewIsBlock(a8) || // EXP-011 §59. The block form ends in `}` too.
270823
+ a8.kind === "crypto-call" || // EXP-011 §60. The parent write's `if` ends in `}` and takes no terminator.
270824
+ a8.kind === "component-object-set" && a8.parent || // EXP-011 §61. The pop's block form ends in `}`; the push ends in `)` and takes its `;`, as does a Reset.
270825
+ a8.kind === "stack-pop"
270826
+ );
269100
270827
  }
269101
270828
  function actionsAwait(actions) {
269102
270829
  return deepActions(actions).some(
@@ -269107,7 +270834,7 @@ ${pad(indent)}}`;
269107
270834
  }
269108
270835
  function blockBody(expanded, indent) {
269109
270836
  return expanded.map(
269110
- (a8) => actionTakesNoTerminator(a8) ? `${pad(indent)}${actionCode(a8, indent)}` : `${pad(indent)}${actionCode(a8, a8.kind === "date-now-read" || a8.kind === "id-new" || a8.kind === "crypto-call" || a8.kind === "branch" || a8.kind === "array-new" || a8.kind === "component-object-set" || a8.kind === "stack-push" || a8.kind === "stack-reset" ? indent : 0)};`
270837
+ (a8) => actionTakesNoTerminator(a8) ? `${pad(indent)}${actionCode(a8, indent)}` : `${pad(indent)}${actionCode(a8, "detached" in a8 && a8.detached === true || a8.kind === "date-now-read" || a8.kind === "id-new" || a8.kind === "crypto-call" || a8.kind === "branch" || a8.kind === "array-new" || a8.kind === "component-object-set" || a8.kind === "stack-push" || a8.kind === "stack-reset" || a8.kind === "jsfun-run" ? indent : 0)};`
269111
270838
  );
269112
270839
  }
269113
270840
  function effectBody(actions, indent) {
@@ -269148,12 +270875,14 @@ ${pad(indent)}}`;
269148
270875
  externalImports.push(`import { ${coreHooks.sort().join(", ")} } from '@nodegx/core/react';`);
269149
270876
  }
269150
270877
  const reactImports = [];
269151
- if (plan.queries.length > 0) reactImports.push("useEffect", "useState");
270878
+ if (plan.queries.length > 0) reactImports.push(...plan.queries.some((q) => queryHasEffect(q)) ? ["useEffect"] : [], "useState");
269152
270879
  if (plan.popups.length > 0 && !reactImports.includes("useState")) reactImports.push("useState");
269153
270880
  if (referencedStateVars.length > 0 && !reactImports.includes("useState")) reactImports.push("useState");
269154
270881
  if ((plan.syncEffects.length > 0 || plan.pushEffects.length > 0 || plan.branchEffects.length > 0 || // EXP-011 §39. A Value Changed is an effect; a Delay owes the unmount cleanup effect.
269155
- plan.valueChangedEffects.length > 0 || // EXP-011 §43. A Record with Fetch unwired is an effect keyed on its Id.
269156
- plan.recordEffects.length > 0 || // EXP-011 §67. A Variable's authored Value is a mount effect.
270882
+ plan.valueChangedEffects.length > 0 || // P18-S119-TICKEDRUN. A ticked input's run is an effect.
270883
+ plan.tickedRuns.length > 0 || // EXP-011 §43, P18-S113-IDBINDREAD. A Record with Fetch unwired is a bind keyed on its Id.
270884
+ recordBinds.length > 0 || // P18-S111-RECORDLIVE. A live Record's row subscribes to the client's copy of its record.
270885
+ liveRecordRows.length > 0 || // EXP-011 §67. A Variable's authored Value is a mount effect.
269157
270886
  plan.variableSeeds.length > 0 || // EXP-011 §71. An Object's own authored property values are a mount effect.
269158
270887
  plan.objectSeeds.length > 0 || // EXP-011 §48. A CSS Definition is a mount effect.
269159
270888
  plan.styleSheets.length > 0 || // EXP-011 §49. A States' wired State input is an effect keyed on the read.
@@ -269162,8 +270891,8 @@ ${pad(indent)}}`;
269162
270891
  reactImports.push("useEffect");
269163
270892
  }
269164
270893
  if (radioNameLocals.size > 0) reactImports.push("useId");
269165
- if (delayRefs.size > 0 || plan.valueChangedEffects.length > 0 || usedKeptLocals.size > 0) reactImports.push("useRef");
269166
- if (plan.task !== void 0 || plan.repeaterItems.length > 0) {
270894
+ if (delayRefs.size > 0 || plan.valueChangedEffects.length > 0 || usedKeptLocals.size > 0 || pulsedRunIds.size > 0 || plan.tickedRuns.length > 0) reactImports.push("useRef");
270895
+ if (plan.task !== void 0 || plan.repeaterItems.length > 0 || plan.fieldMounts.length > 0 || mountChains.length > 0) {
269167
270896
  if (!reactImports.includes("useEffect")) reactImports.push("useEffect");
269168
270897
  if (!reactImports.includes("useRef")) reactImports.push("useRef");
269169
270898
  }
@@ -269188,7 +270917,7 @@ ${pad(indent)}}`;
269188
270917
  for (const read2 of plan.recordReads) stubModule(read2.moduleBase).reads.push(read2);
269189
270918
  for (const [moduleBase, { queries, mutations, reads }] of stubModules) {
269190
270919
  const specifier = `${relRoot}/api/${moduleBase}`;
269191
- const fetchNames = [.../* @__PURE__ */ new Set([...queries.map((q) => q.fetchName), ...reads.map((r) => r.fnName)])].sort();
270920
+ const fetchNames = [.../* @__PURE__ */ new Set([...queries.map((q) => q.fetchName), ...reads.filter((r) => !r.binds).map((r) => r.fnName)])].sort();
269192
270921
  const fnNames = [...new Set(mutations.map((m) => m.fnName))].sort();
269193
270922
  const typeNames = [
269194
270923
  .../* @__PURE__ */ new Set([
@@ -269201,6 +270930,11 @@ ${pad(indent)}}`;
269201
270930
  `import { ${[...fetchNames, ...fnNames, ...typeNames.map((t) => `type ${t}`)].join(", ")} } from '${specifier}';`
269202
270931
  );
269203
270932
  }
270933
+ const clientNames = [
270934
+ ...plan.queries.some((q) => q.wiredClient === true && q.params?.wired !== void 0) ? ["filterValue"] : [],
270935
+ ...liveRecordRows.length > 0 || recordBinds.length > 0 ? ["subscribeRecord"] : []
270936
+ ];
270937
+ if (clientNames.length > 0) internalImports.set(`${relRoot}/api/client`, `import { ${clientNames.join(", ")} } from '${relRoot}/api/client';`);
269204
270938
  if (plan.sessionCalls.length > 0) {
269205
270939
  const specifier = `${relRoot}/api/session`;
269206
270940
  const fnNames = [...new Set(plan.sessionCalls.map((c) => c.fnName))].sort();
@@ -269372,6 +271106,11 @@ ${pad(indent)}}`;
269372
271106
  const specifier = `${relRoot}/${ID_LIB_PATH.replace(/^src\//, "").replace(/\.ts$/, "")}`;
269373
271107
  internalImports.set(specifier, `import { ${[...usedIdHelpers].sort().join(", ")} } from '${specifier}';`);
269374
271108
  }
271109
+ if (pulsedRunIds.size > 0 || tickedRunIds.size > 0) {
271110
+ const specifier = `${relRoot}/${FUNCTION_RUN_LIB_PATH.replace(/^src\//, "").replace(/\.ts$/, "")}`;
271111
+ const helpers = [...pulsedRunIds.size > 0 ? ["functionNode", "runFunction"] : [], ...tickedRunIds.size > 0 ? ["tickedArrival"] : []];
271112
+ internalImports.set(specifier, `import { ${helpers.join(", ")} } from '${specifier}';`);
271113
+ }
269375
271114
  if (usedVariableNames.size > 0) {
269376
271115
  const specifier = `${relRoot}/stores/variables`;
269377
271116
  const names2 = [...usedVariableNames].map((n) => variableByName.get(n).exportName).sort();
@@ -269669,6 +271408,12 @@ ${pad(indent)}}`;
269669
271408
  const roleEvents = ROLE_EVENT_ATTRS[plan.roleOf[node.id]] ?? {};
269670
271409
  for (const [port5, actions] of Object.entries(plan.handlers[node.id] ?? {})) {
269671
271410
  if (port5 === "onChange" && controlVarByNode.has(node.id)) continue;
271411
+ if (port5 === "didMount" && mountHandled.has(node.id)) continue;
271412
+ if (port5 === "didMount" && mountRefusals.has(node.id)) {
271413
+ notes.push(`${plan.path}: signal ${node.id}.didMount dropped: ${mountRefusals.get(node.id)} \u2014 reported`);
271414
+ defer(node.id, 'the "didMount" signal', mountRefusals.get(node.id));
271415
+ continue;
271416
+ }
269672
271417
  const eventAttr = roleEvents[port5] ?? EVENT_ATTRS[port5];
269673
271418
  if (!eventAttr) {
269674
271419
  notes.push(`${plan.path}: signal ${node.id}.${port5} has no DOM event equivalent \u2014 dropped, reported`);
@@ -270276,7 +272021,8 @@ ${pad(indent)}}`;
270276
272021
  const specifier = `../kits/${binding.modulePath.split("/").pop()}`;
270277
272022
  const existing = internalImports.get(specifier);
270278
272023
  const symbols = new Set(existing ? existing.replace(/^import \{ | \} from .*$/g, "").split(", ") : []);
270279
- symbols.add(binding.symbol);
272024
+ const kitLocal = binding.symbol === plan.file.symbol ? `${binding.symbol}Kit` : binding.symbol;
272025
+ symbols.add(kitLocal === binding.symbol ? binding.symbol : `${binding.symbol} as ${kitLocal}`);
270280
272026
  internalImports.set(specifier, `import { ${[...symbols].sort().join(", ")} } from '${specifier}';`);
270281
272027
  const attrs = [];
270282
272028
  for (const param of node.parameters) {
@@ -270341,7 +272087,7 @@ ${pad(indent)}}`;
270341
272087
  );
270342
272088
  }
270343
272089
  const blocks = renderChildBlocks(node.id, childIds, indent + 2).flat();
270344
- return element(binding.symbol, attrs, blocks.length > 0 ? blocks : null, indent, blocks.length > 0);
272090
+ return element(kitLocal, attrs, blocks.length > 0 ? blocks : null, indent, blocks.length > 0);
270345
272091
  };
270346
272092
  const instanceAttrs = (node) => {
270347
272093
  const attrs = [];
@@ -270474,9 +272220,11 @@ ${pad(indent)}}`;
270474
272220
  const outputsType = outputFields.length > 0 ? `{ ${outputFields.join("; ")} }` : "Record<string, never>";
270475
272221
  const seeds = def.signals.map((s) => `${fieldKey(s)}: () => {}`);
270476
272222
  lines.push(`// From the Function node "${def.fnName}" \u2014 the body is preserved verbatim (EXP-003 \xA74).`);
270477
- lines.push(`function ${def.fnName}(${hasInputs ? INPUTS_PARAM : "Inputs"}: ${inputsType}): ${outputsType} {`);
272223
+ const seedRecord = `{${seeds.length > 0 ? ` ${seeds.join(", ")} ` : ""}}`;
272224
+ const outputsParam = def.pulsed ? `, Outputs: ${outputsType} = ${seedRecord}` : "";
272225
+ lines.push(`function ${def.fnName}(${hasInputs ? INPUTS_PARAM : "Inputs"}: ${inputsType}${outputsParam}): ${outputsType} {`);
270478
272226
  if (hasInputs) lines.push(` const Inputs = ${scopeCast(INPUTS_PARAM)};`);
270479
- lines.push(` const Outputs: ${outputsType} = {${seeds.length > 0 ? ` ${seeds.join(", ")} ` : ""}};`);
272227
+ if (!def.pulsed) lines.push(` const Outputs: ${outputsType} = ${seedRecord};`);
270480
272228
  lines.push(" try {");
270481
272229
  if (def.scripts !== void 0 && def.scripts.length > 0) {
270482
272230
  const rows = def.scripts.map((u) => `${fieldKey(u.alias)}: ${PROJECT_SCRIPTS_SYMBOL}.load(${JSON.stringify(u.path)})`);
@@ -270548,12 +272296,39 @@ ${pad(indent)}}`;
270548
272296
  for (const alias of def.mathAliases) {
270549
272297
  lines.push(alias === "pi" ? " const pi = Math.PI;" : ` const ${alias} = Math.${alias};`);
270550
272298
  }
270551
- lines.push(" try {");
270552
- lines.push(` return (${def.body});`);
270553
- lines.push(" } catch (e) {");
270554
- lines.push(` console.error('Expression node ${def.fnName} threw:', e);`);
270555
- lines.push(" return 0;");
270556
- lines.push(" }");
272299
+ const gate = def.loadGate;
272300
+ const unsetTest = gate !== void 0 ? gate.unset.map((n) => `${n} === undefined`).join(" && ") : "";
272301
+ if (gate?.atLoad === "wait") {
272302
+ lines.push(` // Evaluate At Load is unticked: no answer until an input arrives.`);
272303
+ lines.push(` if (${unsetTest}) return null;`);
272304
+ }
272305
+ if (gate?.atLoad === "evaluate" && gate.unset.length === 0) {
272306
+ lines.push(" // Nothing feeds its inputs: the load evaluation over them, which keeps null for a throw or a NaN.");
272307
+ lines.push(" try {");
272308
+ lines.push(` const __value = (${def.body});`);
272309
+ lines.push(" return Number.isNaN(__value) ? null : __value;");
272310
+ lines.push(" } catch {");
272311
+ lines.push(" return null;");
272312
+ lines.push(" }");
272313
+ } else if (gate?.atLoad === "evaluate") {
272314
+ lines.push(" // Until an input arrives this is the load evaluation, which keeps null for a throw or a NaN.");
272315
+ lines.push(` const __unset = ${unsetTest};`);
272316
+ lines.push(" try {");
272317
+ lines.push(` const __value = (${def.body});`);
272318
+ lines.push(" return __unset && Number.isNaN(__value) ? null : __value;");
272319
+ lines.push(" } catch (e) {");
272320
+ lines.push(" if (__unset) return null;");
272321
+ lines.push(` console.error('Expression node ${def.fnName} threw:', e);`);
272322
+ lines.push(" return 0;");
272323
+ lines.push(" }");
272324
+ } else {
272325
+ lines.push(" try {");
272326
+ lines.push(` return (${def.body});`);
272327
+ lines.push(" } catch (e) {");
272328
+ lines.push(` console.error('Expression node ${def.fnName} threw:', e);`);
272329
+ lines.push(" return 0;");
272330
+ lines.push(" }");
272331
+ }
270557
272332
  lines.push("}");
270558
272333
  }
270559
272334
  return lines;
@@ -270629,9 +272404,11 @@ ${pad(indent)}}`;
270629
272404
  internalImports.set(specifier, `import { ${[...usedMediaHelpers].sort().join(", ")} } from '${specifier}';`);
270630
272405
  }
270631
272406
  const usesProjectScripts = Object.values(jsFunByNode).some((def) => referencedJsIds.has(def.nodeId) && (def.scripts?.length ?? 0) > 0);
270632
- if (usesProjectScripts) {
270633
- const specifier = `${relRoot}/${PROJECT_SCRIPT_FILES_PATH.replace(/^src\//, "").replace(/\.ts$/, "")}`;
270634
- internalImports.set(specifier, `import { ${PROJECT_SCRIPTS_SYMBOL} } from '${specifier}';`);
272407
+ const functionFiles = Object.values(jsFunByNode).filter((def) => def.kind === "function" && referencedJsIds.has(def.nodeId)).map((def) => ({ def, file: `src/functions/${plan.file.dir}/${plan.file.fileBase}/${def.fnName}.ts` }));
272408
+ const ownFileIds = new Set(functionFiles.map(({ def }) => def.nodeId));
272409
+ for (const { def, file } of functionFiles) {
272410
+ const from = `${relRoot}/${file.replace(/^src\//, "").replace(/\.ts$/, "")}`;
272411
+ internalImports.set(from, `import { ${def.fnName} } from '${from}';`);
270635
272412
  }
270636
272413
  const importLines = [];
270637
272414
  if (externalImports.length > 0) importLines.push(...externalImports.sort());
@@ -270642,6 +272419,7 @@ ${pad(indent)}}`;
270642
272419
  }
270643
272420
  const body = [];
270644
272421
  for (const def of Object.values(jsFunByNode)) {
272422
+ if (ownFileIds.has(def.nodeId)) continue;
270645
272423
  if (referencedJsIds.has(def.nodeId)) body.push(...jsWrapperLines(def), "");
270646
272424
  else body.push(...refusedSourceLines(def), "");
270647
272425
  }
@@ -270712,7 +272490,7 @@ ${pad(indent)}}`;
270712
272490
  body.push(` ${ident}?: ${prop.tsType};`);
270713
272491
  }
270714
272492
  for (const output of plan.outputProps) body.push(` ${output.prop}?: () => void;`);
270715
- for (const lifted6 of plan.liftedOutputProps) body.push(` ${lifted6.prop}?: (value: ${lifted6.tsType}) => void;`);
272493
+ for (const lifted6 of plan.liftedOutputProps) body.push(` ${lifted6.prop}?: (value: ${lifted6.tsType} | undefined) => void;`);
270716
272494
  if (plan.closesPopup) body.push(" onClose?: (action?: string) => void;");
270717
272495
  if (plan.popsStack) body.push(` /** The Component Stack that pushed this component \u2014 its Pop Component Stack's way back. */`, ` ${PAGE_STACK_ENTRY_PROP}?: PageStackEntryHandle;`);
270718
272496
  if (plan.childSlot !== void 0) body.push(" children?: ReactNode;");
@@ -270773,12 +272551,46 @@ ${pad(indent)}}`;
270773
272551
  }
270774
272552
  for (const [, ref] of delayRefs) body.push(` const ${ref} = useRef<DelayHandle | null>(null);`);
270775
272553
  for (const effect of plan.valueChangedEffects) body.push(` const ${effect.lastLocal} = useRef<unknown>(undefined);`);
270776
- for (const [id, local] of jsLocals) {
272554
+ for (const id of pulsedRunIds) {
272555
+ const def = jsFunByNode[id];
272556
+ body.push(` const ${pulseNamesOf(def).node} = useRef(functionNode<NonNullable<Parameters<typeof ${def.fnName}>[1]>>([${def.signals.map(tsLiteral).join(", ")}])).current;`);
272557
+ }
272558
+ for (const ticked3 of plan.tickedRuns) body.push(` const ${pulseNamesOf(jsFunByNode[ticked3.nodeId]).held} = useRef<unknown[]>([]).current;`);
272559
+ const hookLocalNames = [
272560
+ ...plan.animations.map((a8) => a8.local),
272561
+ ...plan.drags.map((d) => d.local),
272562
+ ...plan.statesMachines.map((m) => m.local),
272563
+ ...plan.scripts.map((s) => s.local),
272564
+ ...plan.runTasks.map((r) => r.local),
272565
+ ...plan.pageStacks.map((s) => s.local),
272566
+ ...screenHooks.map((s) => s.local),
272567
+ ...plan.appErrors.map((b) => b.local),
272568
+ ...plan.streams.map((s) => s.local)
272569
+ ];
272570
+ const codeReads = (code, name) => new RegExp(`(^|[^A-Za-z0-9_$.])${name.replace(/\$/g, "\\$")}(?![A-Za-z0-9_$])`).test(code);
272571
+ const renderLocalLines = [...jsLocals].map(([id, local]) => {
270777
272572
  const def = jsFunByNode[id];
270778
- body.push(` const ${local} = ${def.fnName}(${jsArgsObject(def, "render")});`);
272573
+ return { local, line: ` const ${local} = ${def.fnName}(${jsArgsObject(def, "render")});` };
272574
+ });
272575
+ const afterHooks = /* @__PURE__ */ new Set();
272576
+ for (let grew = true; grew; ) {
272577
+ grew = false;
272578
+ for (const { local, line } of renderLocalLines) {
272579
+ if (afterHooks.has(local)) continue;
272580
+ const rhs = line.slice(line.indexOf("="));
272581
+ if (hookLocalNames.some((name) => codeReads(rhs, name)) || [...afterHooks].some((name) => codeReads(rhs, name))) {
272582
+ afterHooks.add(local);
272583
+ grew = true;
272584
+ }
272585
+ }
270779
272586
  }
272587
+ for (const { local, line } of renderLocalLines) if (!afterHooks.has(local)) body.push(line);
270780
272588
  for (const animation of plan.animations) {
270781
- const options = `{ duration: ${exprCode(animation.duration, "render")}, delay: ${exprCode(animation.delay, "render")}, ease: ${tsLiteral(animation.ease)} }`;
272589
+ const optionCode = (input, expr) => {
272590
+ const code = exprCode(expr, "render");
272591
+ return animation.castToNumber?.includes(input) ? `${SIMPLE_REF.test(code) ? code : `(${code})`} as number` : code;
272592
+ };
272593
+ const options = `{ duration: ${optionCode("duration", animation.duration)}, delay: ${optionCode("delay", animation.delay)}, ease: ${tsLiteral(animation.ease)} }`;
270782
272594
  const arrive2 = animation.arrive !== void 0 ? `, ${handlerArrow(animation.arrive, "()", 2)}` : "";
270783
272595
  body.push(` // ${animation.comment}`, ` const ${animation.local} = useAnimatedValue(${exprCode(animation.target, "render")}, ${options}${arrive2});`);
270784
272596
  }
@@ -270850,6 +272662,7 @@ ${pad(indent)}}`;
270850
272662
  if (listenerLines.length === 0) body.push(` const ${stream.local} = ${spec.hook}(${source}, ${options});`);
270851
272663
  else body.push(` const ${stream.local} = ${spec.hook}(${source}, ${options}, {`, listenerLines.join(",\n"), " });");
270852
272664
  }
272665
+ for (const { local, line } of renderLocalLines) if (afterHooks.has(local)) body.push(line);
270853
272666
  if (plan.task !== void 0) {
270854
272667
  const started = dedupeLocal("started");
270855
272668
  body.push(
@@ -270882,7 +272695,39 @@ ${pad(indent)}}`;
270882
272695
  body.push("");
270883
272696
  }
270884
272697
  for (const query2 of plan.queries) {
270885
- body.push(" useEffect(() => {", ` ${query2.fetchName}().then(${query2.setterName});`, " }, []);", "");
272698
+ const wired = query2.params?.wired;
272699
+ const label2 = nodeById.get(query2.nodeId)?.authoredLabel;
272700
+ const fetched = query2.fetchWired;
272701
+ if (!queryHasEffect(query2)) {
272702
+ body.push(` // ${label2 ? `"${label2}"` : "The query"} asks only when its Fetch is sent (dbcollectionnode2.ts storageFetch); its boxes are unticked.`, "");
272703
+ continue;
272704
+ }
272705
+ const once = fetched !== void 0 && !fetched.onValues;
272706
+ if (query2.params !== void 0 && wired !== void 0 && query2.wiredValues !== void 0) {
272707
+ const values = query2.wiredValues;
272708
+ const codes = values.map((v) => exprCode(v.expr, "render"));
272709
+ const locals = codes.map((code, i) => {
272710
+ if (/^[A-Za-z_$][\w$]*$/.test(code)) return code;
272711
+ let local = `${propIdentifier(values[i].name)}Value`;
272712
+ while (codes.some((c) => new RegExp(`\\b${local.replace(/\$/g, "\\$")}\\b`).test(c))) local = `${local}_`;
272713
+ return local;
272714
+ });
272715
+ const localOf = (name) => locals[values.findIndex((v) => v.name === name)];
272716
+ const valueCode = (name, date3) => query2.wiredClient ? `filterValue(${localOf(name)}${date3 ? ", true" : ""})` : localOf(name);
272717
+ const deps = [...new Set(values.flatMap((v) => effectDeps(v.expr)))].join(", ");
272718
+ body.push(
272719
+ once ? ` // ${label2 ? `"${label2}"` : "The query"} asks once as it is made (its Class or Query settings box is ticked); a new ${values.map((v) => `"${v.name}"`).join(" or ")} waits for its Fetch.` : ` // ${label2 ? `"${label2}"` : "The query"} asks again whenever ${values.map((v) => `"${v.name}"`).join(" or ")} changes (dbcollectionnode2.ts setQueryParameter).`,
272720
+ " useEffect(() => {",
272721
+ ...codes.flatMap((code, i) => code === locals[i] ? [] : [` const ${locals[i]} = ${code};`]),
272722
+ ` const where = ${wiredWhereExpr(wired, locals, valueCode)};`,
272723
+ ` ${query2.fetchName}(${queryParamsLiteral(query2.params, "where")}).then(${query2.setterName});`,
272724
+ ...once ? [" // eslint-disable-next-line react-hooks/exhaustive-deps -- once, as the node is made", " }, []);"] : [` }, [${deps}]);`],
272725
+ ""
272726
+ );
272727
+ continue;
272728
+ }
272729
+ const params = query2.params === void 0 ? "" : queryParamsLiteral(query2.params);
272730
+ body.push(" useEffect(() => {", ` ${query2.fetchName}(${params}).then(${query2.setterName});`, " }, []);", "");
270886
272731
  }
270887
272732
  for (const seed of plan.variableSeeds) {
270888
272733
  body.push(` // ${seed.comment}`, " useEffect(() => {", ...effectBody([variableSeedAction(seed)], 4), " }, []);", "");
@@ -270963,11 +272808,43 @@ ${pad(indent)}}`;
270963
272808
  );
270964
272809
  }
270965
272810
  }
272811
+ for (const mount2 of plan.fieldMounts) {
272812
+ const chain = plan.changeHandlers[mount2.inputId] ?? [];
272813
+ if (chain.length === 0) continue;
272814
+ const once = dedupeLocal("announced");
272815
+ const lines = [];
272816
+ let code = tsLiteral(mount2.start);
272817
+ if (mount2.source !== void 0) {
272818
+ const arrival = dedupeLocal("arrival");
272819
+ code = dedupeLocal("mountValue");
272820
+ lines.push(
272821
+ ` const ${arrival} = ${exprCode(mount2.source, "render")};`,
272822
+ ` const ${code} = ${arrival} === undefined ? ${tsLiteral(mount2.start)} : ${arrival} === null ? '' : String(${arrival});`
272823
+ );
272824
+ }
272825
+ announcedCode = { inputId: mount2.inputId, code };
272826
+ const chainLines = effectBody(chain, 4);
272827
+ announcedCode = void 0;
272828
+ body.push(
272829
+ ` // ${mount2.comment}`,
272830
+ ` const ${once} = useRef(false);`,
272831
+ " useEffect(() => {",
272832
+ ` if (${once}.current) return;`,
272833
+ ` ${once}.current = true;`,
272834
+ ...lines,
272835
+ ...chainLines,
272836
+ " // eslint-disable-next-line react-hooks/exhaustive-deps -- once, as the field mounts",
272837
+ " }, []);",
272838
+ ""
272839
+ );
272840
+ }
270966
272841
  for (const push of plan.pushEffects) {
270967
272842
  const deps = [...effectDeps(push.expr), push.prop].join(", ");
272843
+ const code = exprCode(push.expr, "render");
272844
+ const pushed = push.castTo === void 0 ? code : `${SIMPLE_REF.test(code) ? code : `(${code})`} as ${push.castTo} | undefined`;
270968
272845
  body.push(
270969
272846
  " useEffect(() => {",
270970
- ` ${push.prop}?.(${exprCode(push.expr, "render")});`,
272847
+ ` ${push.prop}?.(${pushed});`,
270971
272848
  ` }, [${deps}]);`,
270972
272849
  ""
270973
272850
  );
@@ -270977,10 +272854,27 @@ ${pad(indent)}}`;
270977
272854
  const deps = effectDeps(action.cond).join(", ");
270978
272855
  body.push(` // ${effect.comment}`, " useEffect(() => {", ...effectBody([action], 4), ` }, [${deps}]);`, "");
270979
272856
  }
270980
- for (const effect of plan.recordEffects) {
270981
- const action = effect.action;
272857
+ for (const { effect, action, read: read2, row } of recordBinds) {
270982
272858
  const deps = effectDeps(action.id).join(", ");
270983
- body.push(` // ${effect.comment}`, " useEffect(() => {", ...effectBody([action], 4), ` }, [${deps}]);`, "");
272859
+ const idCode = action.coerceId === "string" ? `String(${exprCode(action.id, "render")} ?? '')` : exprCode(action.id, "render");
272860
+ const bind2 = `subscribeRecord<${read2.typeName}>(${read2.idLocal}, ${stateSetterOf(row.name)})`;
272861
+ body.push(
272862
+ ` // ${effect.comment}`,
272863
+ " useEffect(() => {",
272864
+ ` const ${read2.idLocal} = ${idCode};`,
272865
+ ...action.id.kind === "literal" ? [] : [` if (${read2.idLocal} === undefined || ${read2.idLocal} === null || ${read2.idLocal} === '') return undefined;`],
272866
+ ...effect.follows ? [` return ${bind2};`] : [' // "Record properties" is unticked: the copy as it is at the bind, and no later write.', ` ${bind2}();`, " return undefined;"],
272867
+ ` }, [${deps}]);`,
272868
+ ""
272869
+ );
272870
+ }
272871
+ for (const { read: read2, row } of liveRecordRows) {
272872
+ const label2 = nodeById.get(read2.nodeId)?.authoredLabel;
272873
+ body.push(
272874
+ ` // ${label2 ? `"${label2}"` : "The Record"} shows the app's one copy of its record, so a write anywhere in the app reaches it (dbmodelnode2.ts, "Record properties" ticked).`,
272875
+ ` useEffect(() => (${row.name}?.id === undefined ? undefined : subscribeRecord<${read2.typeName}>(${row.name}.id, ${stateSetterOf(row.name)})), [${row.name}?.id]);`,
272876
+ ""
272877
+ );
270984
272878
  }
270985
272879
  for (const [, ref] of delayRefs) {
270986
272880
  body.push(" useEffect(() => () => {", ` stopDelay(${ref});`, " }, []);", "");
@@ -271023,6 +272917,32 @@ ${pad(indent)}}`;
271023
272917
  ""
271024
272918
  );
271025
272919
  }
272920
+ for (const ticked3 of plan.tickedRuns) {
272921
+ const deps = [...new Set(ticked3.watch.flatMap(effectDeps))].join(", ");
272922
+ body.push(
272923
+ ` // ${ticked3.comment}`,
272924
+ " useEffect(() => {",
272925
+ ` if (!tickedArrival(${tickedRecordArgs(ticked3.nodeId)})) return;`,
272926
+ ...effectBody([ticked3.action], 4),
272927
+ ` }, [${deps}]);`,
272928
+ ""
272929
+ );
272930
+ }
272931
+ for (const mount2 of mountChains) {
272932
+ const node = nodeById.get(mount2.nodeId);
272933
+ const mounted = dedupeLocal("mounted");
272934
+ body.push(
272935
+ ` // ${node.authoredLabel ?? node.type} \u2014 Did Mount: once, when it mounts, as the running app sends it.`,
272936
+ ` const ${mounted} = useRef(false);`,
272937
+ " useEffect(() => {",
272938
+ ` if (${mounted}.current) return;`,
272939
+ ` ${mounted}.current = true;`,
272940
+ ...effectBody(mount2.actions, 4),
272941
+ " // eslint-disable-next-line react-hooks/exhaustive-deps -- once, like the element's own Did Mount",
272942
+ " }, []);",
272943
+ ""
272944
+ );
272945
+ }
271026
272946
  const actionExprsOf = (a8) => {
271027
272947
  switch (a8.kind) {
271028
272948
  case "emit":
@@ -271060,11 +272980,16 @@ ${pad(indent)}}`;
271060
272980
  return [a8.items];
271061
272981
  case "runtasks-abort":
271062
272982
  return [];
272983
+ case "query-fetch":
272984
+ return a8.values.map((v) => v.expr);
271063
272985
  // EXP-011 §58. The data; the config and listeners are walked off the plan.
271064
272986
  case "stream-action":
271065
272987
  return a8.value !== void 0 ? [a8.value] : [];
271066
272988
  case "jsfun-run":
271067
272989
  return [...jsArgExprs(a8.nodeId), ...a8.then.flatMap(actionExprsOf)];
272990
+ case "jsfun-pulses":
272991
+ case "jsfun-pulse":
272992
+ return a8.then.flatMap(actionExprsOf);
271068
272993
  case "http-call":
271069
272994
  // EXP-011 §41. The same, one node over.
271070
272995
  case "cloud-call":
@@ -271088,7 +273013,8 @@ ${pad(indent)}}`;
271088
273013
  case "api-call":
271089
273014
  return [
271090
273015
  ...a8.args.flatMap((arg) => arg.kind === "expr" ? [arg.expr] : arg.props.map((p) => p.expr)),
271091
- ...a8.then.flatMap(actionExprsOf)
273016
+ ...a8.then.flatMap(actionExprsOf),
273017
+ ...a8.failThen.flatMap(actionExprsOf)
271092
273018
  ];
271093
273019
  // EXP-011 Tier 1.3. The Read reads nothing of its own; the chain is where the expressions
271094
273020
  // are, and a reader of this function wants them (the `usesPayload` test below is one).
@@ -271188,12 +273114,14 @@ ${pad(indent)}}`;
271188
273114
  for (const action of expandActions(receiver.actions)) body.push(` ${actionCode(action)};`);
271189
273115
  body.push(" });", "");
271190
273116
  }
271191
- const providedJsx = printsRecord && !(jsxLines.length === 1 && jsxLines[0] === " null") ? [
273117
+ const removedRoot = jsxLines.length === 1 ? /^\s*\{\/\* (.*) \*\/\}$/.exec(jsxLines[0]) : null;
273118
+ const providedJsx = printsRecord && removedRoot === null && !(jsxLines.length === 1 && jsxLines[0] === " null") ? [
271192
273119
  ` <ParentComponentObjectContext.Provider value={${plan.componentObject.local}}>`,
271193
273120
  ...jsxLines.map((line) => line.trim().length === 0 ? line : ` ${line}`),
271194
273121
  " </ParentComponentObjectContext.Provider>"
271195
273122
  ] : jsxLines;
271196
273123
  if (jsxLines.length === 1 && jsxLines[0] === " null") body.push(...preReturnComment, " return null;", "}");
273124
+ else if (removedRoot !== null) body.push(...preReturnComment, ` // ${removedRoot[1]}`, " return null;", "}");
271197
273125
  else body.push(...preReturnComment, " return (", ...providedJsx, " );", "}");
271198
273126
  if (usesRowField) {
271199
273127
  body.push(
@@ -271263,6 +273191,7 @@ ${printDecls(query2.decls, " ")}
271263
273191
  files[`${baseDir}/${plan.file.fileBase}.module.css`] = GENERATED_CSS2 + "\n" + cssBlocks.join("\n\n") + "\n";
271264
273192
  }
271265
273193
  for (const script2 of plan.scripts) files[script2.file] = scriptFileSource(script2, plan.path, plan.legacyPath);
273194
+ for (const { def, file } of functionFiles) files[file] = functionFileSource(def, jsWrapperLines(def), plan.path);
271266
273195
  return {
271267
273196
  files,
271268
273197
  notes,
@@ -271270,6 +273199,8 @@ ${printDecls(query2.decls, " ")}
271270
273199
  utilHelpers: usedUtilHelpers,
271271
273200
  idHelpers: usedIdHelpers,
271272
273201
  timerHelpers: usedTimerHelpers,
273202
+ // EXP-011 §86.
273203
+ functionRunLib: pulsedRunIds.size > 0 || tickedRunIds.size > 0,
271273
273204
  // EXP-011 §49. `states.ts` imports `animate.ts`, so a States alone earns both modules.
271274
273205
  animateLib: plan.animations.length > 0 || plan.statesMachines.length > 0,
271275
273206
  statesLib: plan.statesMachines.length > 0,
@@ -271366,6 +273297,24 @@ function scriptFileSource(script2, componentPath, legacyPath) {
271366
273297
  ];
271367
273298
  return lines.join("\n");
271368
273299
  }
273300
+ var GENERATED_FUNCTION_TS = "// @nodegx:generated (function node \u2014 provenance markers complete in EXP-007)";
273301
+ function functionFileSource(def, wrapper, componentPath) {
273302
+ const safe = (text2) => text2.split("*/").join("* /");
273303
+ const signature = `function ${def.fnName}(`;
273304
+ const lines = [
273305
+ "// @ts-nocheck",
273306
+ GENERATED_FUNCTION_TS,
273307
+ "//",
273308
+ `// The Function node "${safe(def.fnName)}" (node ${safe(def.nodeId)}) of ${safe(componentPath)}, in a file of its own.`,
273309
+ "// Type checking is off for this one file, as for a Script node's: the body is the author's JavaScript, not",
273310
+ "// TypeScript. The signature is typed, and every call to it is checked in the component that makes it.",
273311
+ ...(def.scripts?.length ?? 0) > 0 ? [`import { ${PROJECT_SCRIPTS_SYMBOL} } from '../../../${PROJECT_SCRIPT_FILES_PATH.replace(/^src\//, "").replace(/\.ts$/, "")}';`] : [],
273312
+ "",
273313
+ ...wrapper.map((line) => line.startsWith(signature) ? `export ${line}` : line),
273314
+ ""
273315
+ ];
273316
+ return lines.join("\n");
273317
+ }
271369
273318
  function preOrder(plan) {
271370
273319
  if (!plan.rootId) return [];
271371
273320
  const out = [];
@@ -271424,7 +273373,7 @@ function jsxAttr(name, value) {
271424
273373
  return `${name}={${value}}`;
271425
273374
  }
271426
273375
  function jsxText(text2) {
271427
- if (/[{}<>]/.test(text2)) return `{${JSON.stringify(text2)}}`;
273376
+ if (/[{}<>&\r\n]/.test(text2) || /^\s|\s$/.test(text2)) return `{${JSON.stringify(text2)}}`;
271428
273377
  return text2;
271429
273378
  }
271430
273379
  function withFallback(code, fallback) {
@@ -271506,6 +273455,14 @@ function filledArcPath(x, y, radius, startAngle, endAngle) {
271506
273455
  svgNumber(start3.y)
271507
273456
  ].join(" ");
271508
273457
  }
273458
+ function runIsPulsed(action) {
273459
+ return action.then.some((a8) => a8.kind === "jsfun-pulses");
273460
+ }
273461
+ function pulseNamesOf(def) {
273462
+ const camel = def.fnName.replace(/_+([A-Za-z0-9])/g, (_m, c) => c.toUpperCase()).replace(/_+$/, "");
273463
+ const base = camel.charAt(0).toLowerCase() + camel.slice(1);
273464
+ return { run: `${base}Run`, node: `${base}Node`, pulse: `${base}Pulse`, held: `${base}Held` };
273465
+ }
271509
273466
 
271510
273467
  // ../nodegx-export/src/emit/dateLib.ts
271511
273468
  var DATE_LIB_PATH = "src/lib/date.ts";
@@ -272067,8 +274024,8 @@ var coverage_ledger_default = {
272067
274024
  },
272068
274025
  {
272069
274026
  typeName: "DbCollection2",
272070
- status: "stubbed",
272071
- exemption: "typed api stub + useState/useEffect \u2014 the backend seam, by design (phase README: no backend code export)"
274027
+ status: "translated",
274028
+ note: "Was `stubbed` with \"no backend code export\", which EXP-009 overtook: the query is a fetch<Type>() in src/api/<class>.ts that calls the EXP-009 client's query() against the project's NodeGX backend, held in useState and run in a useEffect at mount, or in the handler of whatever sends Fetch (P18-S116-QUERYFETCH, the-query-fetch.test.ts). Reclassified 2026-10-17 by reading the api module emitted for tests/fixtures/puppy-test-3 (fetchPuppies() returns query<Puppy>('Puppy')); the internal disposition is still named 'stubbed' (visual.test.ts)."
272072
274029
  },
272073
274030
  {
272074
274031
  typeName: "DbModel",
@@ -273377,6 +275334,17 @@ function emitApp(ir, catalog2) {
273377
275334
  notes.push(cloud);
273378
275335
  projectNotes.push(cloud);
273379
275336
  }
275337
+ for (const store2 of project2.stores) {
275338
+ for (const n of store2.notes) {
275339
+ const line = `${n.componentPath}: node ${n.nodeId} (net.noodl.GlobalStore, store "${store2.name}"): ${n.text}`;
275340
+ notes.push(line);
275341
+ projectNotes.push(line);
275342
+ }
275343
+ }
275344
+ for (const line of routerNotes(ir)) {
275345
+ notes.push(line);
275346
+ projectNotes.push(line);
275347
+ }
273380
275348
  const unreachable = /* @__PURE__ */ new Set();
273381
275349
  for (const legacy of project2.reachability.unreachable) {
273382
275350
  const path43 = legacy.replace(/^\//, "");
@@ -273389,6 +275357,7 @@ function emitApp(ir, catalog2) {
273389
275357
  const utilHelpersUsed = /* @__PURE__ */ new Set();
273390
275358
  const idHelpersUsed = /* @__PURE__ */ new Set();
273391
275359
  const timerHelpersUsed = /* @__PURE__ */ new Set();
275360
+ let functionRunLibUsed = false;
273392
275361
  let animateLibUsed = false;
273393
275362
  let statesLibUsed = false;
273394
275363
  let scriptLibUsed = false;
@@ -273459,6 +275428,7 @@ function emitApp(ir, catalog2) {
273459
275428
  for (const helper of emitted.dateHelpers) dateHelpersUsed.add(helper);
273460
275429
  for (const helper of emitted.utilHelpers) utilHelpersUsed.add(helper);
273461
275430
  for (const helper of emitted.idHelpers) idHelpersUsed.add(helper);
275431
+ if (emitted.functionRunLib) functionRunLibUsed = true;
273462
275432
  for (const helper of emitted.timerHelpers) timerHelpersUsed.add(helper);
273463
275433
  if (emitted.animateLib) animateLibUsed = true;
273464
275434
  if (emitted.statesLib) statesLibUsed = true;
@@ -273496,6 +275466,9 @@ function emitApp(ir, catalog2) {
273496
275466
  if (idHelpersUsed.size > 0) {
273497
275467
  files[ID_LIB_PATH] = GENERATED_MODULE_TS + idLibSource();
273498
275468
  }
275469
+ if (functionRunLibUsed) {
275470
+ files[FUNCTION_RUN_LIB_PATH] = GENERATED_MODULE_TS + functionRunLibSource();
275471
+ }
273499
275472
  if (timerHelpersUsed.size > 0) {
273500
275473
  files[TIMER_LIB_PATH] = GENERATED_MODULE_TS + timerLibSource();
273501
275474
  }
@@ -273647,7 +275620,7 @@ function apiModules(ir, project2) {
273647
275620
  if (entry === void 0) {
273648
275621
  byCollection.set(
273649
275622
  collectionName4,
273650
- entry = { typeName: typeName3, moduleBase, querySites: [], readSites: [], mutations: /* @__PURE__ */ new Map(), writes: /* @__PURE__ */ new Map(), reads: /* @__PURE__ */ new Map() }
275623
+ entry = { typeName: typeName3, moduleBase, querySites: [], queryParams: false, readSites: [], mutations: /* @__PURE__ */ new Map(), writes: /* @__PURE__ */ new Map(), reads: /* @__PURE__ */ new Map() }
273651
275624
  );
273652
275625
  }
273653
275626
  return entry;
@@ -273657,11 +275630,14 @@ function apiModules(ir, project2) {
273657
275630
  const entry = moduleFor(query2.collectionName, query2.typeName, query2.moduleBase);
273658
275631
  entry.fetchName = query2.fetchName;
273659
275632
  entry.querySites.push({ componentPath: plan.path, nodeId: query2.nodeId });
275633
+ if (query2.params !== void 0) entry.queryParams = true;
273660
275634
  }
273661
275635
  for (const read2 of plan.recordReads) {
273662
275636
  const entry = moduleFor(read2.collectionName, read2.typeName, read2.moduleBase);
273663
- entry.fetchOneName = read2.fnName;
273664
- entry.readSites.push({ componentPath: plan.path, nodeId: read2.nodeId });
275637
+ if (!read2.binds) {
275638
+ entry.fetchOneName = read2.fnName;
275639
+ entry.readSites.push({ componentPath: plan.path, nodeId: read2.nodeId });
275640
+ }
273665
275641
  for (const column of read2.reads) {
273666
275642
  if (!entry.reads.has(column.name)) entry.reads.set(column.name, column.tsType);
273667
275643
  }
@@ -273682,6 +275658,8 @@ function apiModules(ir, project2) {
273682
275658
  const hasSubscriptions = project2.plans.some((plan) => plan.streams.some((stream) => stream.kind === "subscription"));
273683
275659
  const hasSignIns = project2.plans.some((plan) => plan.streams.some((stream) => stream.kind === "signin"));
273684
275660
  const hasReturnLeg = hasSignIns || project2.plans.some((plan) => plan.sessionCalls.some((call5) => call5.verb === "magic-link"));
275661
+ const liveRecords = project2.plans.some((plan) => plan.recordReads.some((read2) => read2.live || read2.binds));
275662
+ const wiredFilters = project2.plans.some((plan) => plan.queries.some((query2) => query2.params?.wired !== void 0 && query2.wiredClient === true));
273685
275663
  const hasApi = byCollection.size > 0 || hasSessionCalls || hasCloudCalls || hasFileOps || hasSubscriptions || hasSignIns;
273686
275664
  const notes = [];
273687
275665
  const backend = ir.project.cloudservices;
@@ -273704,7 +275682,7 @@ function apiModules(ir, project2) {
273704
275682
  let anyFileColumn = false;
273705
275683
  for (const [collectionName4, module2] of byCollection) {
273706
275684
  const schema = ir.project.collections.find((c) => c.name === collectionName4);
273707
- const { typeName: typeName3, moduleBase, fetchName, querySites, fetchOneName, readSites, mutations, writes, reads } = module2;
275685
+ const { typeName: typeName3, moduleBase, fetchName, querySites, queryParams, fetchOneName, readSites, mutations, writes, reads } = module2;
273708
275686
  const columns = /* @__PURE__ */ new Map();
273709
275687
  for (const col of schema?.columns ?? []) columns.set(col.name, tsColumnType(col.type));
273710
275688
  for (const [name, tsType] of writes) if (!columns.has(name)) columns.set(name, tsType);
@@ -273726,16 +275704,20 @@ ${fields.join("\n")}${fields.length > 0 ? "\n" : ""}}
273726
275704
  ${querySites.map((s) => siteLine(s, "DbCollection2")).join("\n")}
273727
275705
  * Fetches the \`${collectionName4}\` collection from the project's NodeGX backend
273728
275706
  * (src/api/client.ts); the export report lists every call site.
273729
- */
273730
- export async function ${fetchName}(): Promise<${typeName3}[]> {
275707
+ ` + (queryParams ? ` * Each site passes its own filter, order and limit, as the running app sends them.
275708
+ ` : "") + ` */
275709
+ ` + (queryParams ? `export async function ${fetchName}(params?: QueryParams): Promise<${typeName3}[]> {
275710
+ return query<${typeName3}>(${tsStringLiteral(collectionName4)}, params);
275711
+ }
275712
+ ` : `export async function ${fetchName}(): Promise<${typeName3}[]> {
273731
275713
  return query<${typeName3}>(${tsStringLiteral(collectionName4)});
273732
275714
  }
273733
- ` : `/**
275715
+ `) : `/**
273734
275716
  ${querySites.map((s) => siteLine(s, "DbCollection2")).join("\n")}
273735
275717
  * fetched the \`${collectionName4}\` collection from the project's NodeGX backend. Connect this to your
273736
275718
  * own data source; the export report lists every call site.
273737
275719
  */
273738
- export async function ${fetchName}(): Promise<${typeName3}[]> {
275720
+ export async function ${fetchName}(${queryParams ? "_params?: unknown" : ""}): Promise<${typeName3}[]> {
273739
275721
  return [];
273740
275722
  }
273741
275723
  `
@@ -273824,9 +275806,11 @@ export async function ${fnName}${signature} {
273824
275806
  verb === "create" ? "create" : verb === "update" ? "update" : verb === "add-relation" ? "addRelation" : verb === "remove-relation" ? "removeRelation" : "remove"
273825
275807
  );
273826
275808
  }
273827
- const importLine = `import { ${[...clientImports].sort().join(", ")} } from './client';
275809
+ const clientNames = [...[...clientImports].sort(), ...fetchName !== void 0 && queryParams ? ["type QueryParams"] : []];
275810
+ const importLine = clientNames.length === 0 ? "" : `import { ${clientNames.join(", ")} } from './client';
273828
275811
  `;
273829
- stubs.push([`src/api/${moduleBase}.ts`, GENERATED_MODULE_TS + importLine + filesImport + "\n" + parts.join("\n")]);
275812
+ const imports = importLine + filesImport;
275813
+ stubs.push([`src/api/${moduleBase}.ts`, GENERATED_MODULE_TS + (imports === "" ? "" : imports + "\n") + parts.join("\n")]);
273830
275814
  } else {
273831
275815
  stubs.push([`src/api/${moduleBase}.ts`, GENERATED_TS4 + (filesImport === "" ? "" : filesImport + "\n") + parts.join("\n")]);
273832
275816
  }
@@ -273843,7 +275827,7 @@ export async function ${fnName}${signature} {
273843
275827
  const files = filesModule(project2, backend, siteLine, anyFileColumn);
273844
275828
  if (files !== null) stubs.push(files);
273845
275829
  if (backend !== void 0 && hasApi) {
273846
- stubs.push(["src/api/client.ts", clientModule(backend, hasReturnLeg)]);
275830
+ stubs.push(["src/api/client.ts", clientModule(backend, hasReturnLeg, liveRecords, wiredFilters)]);
273847
275831
  stubs.push([".env.example", envExample(backend)]);
273848
275832
  }
273849
275833
  return { files: stubs, notes, usesBackend: hasApi };
@@ -274449,7 +276433,87 @@ function envLine(name, value) {
274449
276433
  return `${name}=${value.replace(/[\r\n]/g, "")}
274450
276434
  `;
274451
276435
  }
274452
- function clientModule(backend, returnLeg) {
276436
+ var PLAIN_UPDATE_SOURCE = `export async function update<T extends { id: string }>(collection: string, id: string, data: Partial<T>): Promise<T> {
276437
+ // The wire answers a PUT with the changed fields only (\`updatedAt\`), so the
276438
+ // caller's own data is the record the graph continues with.
276439
+ const response = await request<Record<string, unknown>>(\`/classes/\${collection}/\${encodeURIComponent(id)}\`, {
276440
+ method: 'PUT',
276441
+ body: data
276442
+ });
276443
+ const { objectId: _objectId, ...fields } = response;
276444
+ return { ...data, ...fields, id } as unknown as T;
276445
+ }
276446
+ `;
276447
+ var HELD_RECORDS_SOURCE = `
276448
+ /**
276449
+ * The app's one copy of each record, keyed by its id alone as the running app keys them
276450
+ * (\`Model.get(id)\`). Every read merges into it field by field, as the running app's
276451
+ * \`_fromJSON\` does, and nothing removes a field. A Record shows this copy, so a write
276452
+ * anywhere in the app reaches it at once.
276453
+ */
276454
+ type HeldRecord = { id: string } & Record<string, unknown>;
276455
+ const heldRecords = new Map<string, HeldRecord>();
276456
+ const recordListeners = new Map<string, Set<(row: HeldRecord) => void>>();
276457
+
276458
+ function holdRecord<T extends { id: string }>(fields: T): T {
276459
+ const row = { ...heldRecords.get(fields.id), ...fields } as HeldRecord;
276460
+ heldRecords.set(fields.id, row);
276461
+ recordListeners.get(fields.id)?.forEach((listener) => listener(row));
276462
+ return row as unknown as T;
276463
+ }
276464
+
276465
+ /** Hands \`listener\` the record's copy now, and again after every write to it, until the returned function is called. */
276466
+ export function subscribeRecord<T extends { id: string }>(id: string, listener: (row: T) => void): () => void {
276467
+ const call = listener as unknown as (row: HeldRecord) => void;
276468
+ const listeners = recordListeners.get(id) ?? new Set<(row: HeldRecord) => void>();
276469
+ recordListeners.set(id, listeners);
276470
+ listeners.add(call);
276471
+ const held = heldRecords.get(id);
276472
+ if (held !== undefined) call(held);
276473
+ return () => {
276474
+ listeners.delete(call);
276475
+ };
276476
+ }
276477
+ `;
276478
+ var HELD_UPDATE_SOURCE = `export async function update<T extends { id: string }>(collection: string, id: string, data: Partial<T>): Promise<T> {
276479
+ // The running app writes the record before the save and puts back what it held if the
276480
+ // save fails, so a Record showing it moves at the press. The wire answers a PUT with
276481
+ // the changed fields only (\`updatedAt\`), so the caller's own data is the record the
276482
+ // graph continues with.
276483
+ const held: Record<string, unknown> = heldRecords.get(id) ?? {};
276484
+ const before: Record<string, unknown> = {};
276485
+ for (const key of Object.keys(data)) before[key] = held[key];
276486
+ holdRecord({ ...data, id });
276487
+ let response: Record<string, unknown>;
276488
+ try {
276489
+ response = await request<Record<string, unknown>>(\`/classes/\${collection}/\${encodeURIComponent(id)}\`, {
276490
+ method: 'PUT',
276491
+ body: data
276492
+ });
276493
+ } catch (error) {
276494
+ holdRecord({ ...before, id });
276495
+ throw error;
276496
+ }
276497
+ const { objectId: _objectId, ...fields } = response;
276498
+ holdRecord({ ...fields, id });
276499
+ return { ...data, ...fields, id } as unknown as T;
276500
+ }
276501
+ `;
276502
+ var FILTER_VALUE_SOURCE = `
276503
+ /**
276504
+ * A filter value as the running app puts it on the wire: a Date goes as the backend's date envelope, and so does a
276505
+ * date written as text when its column is a Date. Anything else goes as it is.
276506
+ */
276507
+ export function filterValue(value: unknown, dateColumn = false): unknown {
276508
+ if (value instanceof Date) return { __type: 'Date', iso: value.toISOString() };
276509
+ if (dateColumn && value !== null && value !== undefined) {
276510
+ const asDate = new Date(String(value));
276511
+ if (!Number.isNaN(asDate.getTime())) return { __type: 'Date', iso: asDate.toISOString() };
276512
+ }
276513
+ return value;
276514
+ }
276515
+ `;
276516
+ function clientModule(backend, returnLeg, liveRecords, wiredFilters = false) {
274453
276517
  return GENERATED_CLIENT_TS + `/**
274454
276518
  * The NodeGX backend client \u2014 the Parse wire protocol the running app speaks
274455
276519
  * (noodl-runtime's ParseWireAdapter), reduced to what this app's calls need:
@@ -274601,7 +276665,7 @@ function fromWire<T extends { id: string }>(record: Record<string, unknown>): T
274601
276665
  for (const [key, value] of Object.entries(fields)) row[key] = fromWireValue(value);
274602
276666
  return row as unknown as T;
274603
276667
  }
274604
-
276668
+ ${liveRecords ? HELD_RECORDS_SOURCE : ""}${wiredFilters ? FILTER_VALUE_SOURCE : ""}
274605
276669
  export interface QueryParams {
274606
276670
  where?: unknown;
274607
276671
  sort?: string[];
@@ -274622,13 +276686,13 @@ export async function query<T extends { id: string }>(collection: string, params
274622
276686
  skip: params.skip
274623
276687
  }
274624
276688
  });
274625
- return response.results.map((record) => fromWire<T>(record));
276689
+ return response.results.map((record) => ${liveRecords ? "holdRecord(fromWire<T>(record))" : "fromWire<T>(record)"});
274626
276690
  }
274627
276691
 
274628
276692
  /** One record by Id \u2014 the runtime's own \`GET /classes/<collection>/<id>\` (ParseWireAdapter.fetch). */
274629
276693
  export async function fetchOne<T extends { id: string }>(collection: string, id: string): Promise<T> {
274630
276694
  const response = await request<Record<string, unknown>>(\`/classes/\${collection}/\${encodeURIComponent(id)}\`);
274631
- return fromWire<T>(response);
276695
+ return ${liveRecords ? "holdRecord(fromWire<T>(response))" : "fromWire<T>(response)"};
274632
276696
  }
274633
276697
 
274634
276698
  export async function create<T extends { id: string }>(collection: string, data: Partial<T>): Promise<T> {
@@ -274636,20 +276700,10 @@ export async function create<T extends { id: string }>(collection: string, data:
274636
276700
  method: 'POST',
274637
276701
  body: data
274638
276702
  });
274639
- return fromWire<T>({ ...data, ...response });
274640
- }
274641
-
274642
- export async function update<T extends { id: string }>(collection: string, id: string, data: Partial<T>): Promise<T> {
274643
- // The wire answers a PUT with the changed fields only (\`updatedAt\`), so the
274644
- // caller's own data is the record the graph continues with.
274645
- const response = await request<Record<string, unknown>>(\`/classes/\${collection}/\${encodeURIComponent(id)}\`, {
274646
- method: 'PUT',
274647
- body: data
274648
- });
274649
- const { objectId: _objectId, ...fields } = response;
274650
- return { ...data, ...fields, id } as unknown as T;
276703
+ return ${liveRecords ? "holdRecord(fromWire<T>({ ...data, ...response }))" : "fromWire<T>({ ...data, ...response })"};
274651
276704
  }
274652
276705
 
276706
+ ${liveRecords ? HELD_UPDATE_SOURCE : PLAIN_UPDATE_SOURCE}
274653
276707
  export async function remove(collection: string, id: string): Promise<void> {
274654
276708
  await request<unknown>(\`/classes/\${collection}/\${encodeURIComponent(id)}\`, { method: 'DELETE' });
274655
276709
  }
@@ -275764,6 +277818,11 @@ function byType() {
275764
277818
  }
275765
277819
  return cachedByType;
275766
277820
  }
277821
+ var cachedShipped;
277822
+ function shippedTypeNames() {
277823
+ if (!cachedShipped) cachedShipped = new Set(shippedCatalog.nodes.map((n) => n.typeName));
277824
+ return cachedShipped;
277825
+ }
275767
277826
  function allExamples() {
275768
277827
  return shippedCatalog.examples ?? [];
275769
277828
  }
@@ -276391,39 +278450,6 @@ function validateComponentPath(path43) {
276391
278450
  }
276392
278451
 
276393
278452
  // src/project/runOnChangePins.ts
276394
- var projectWillBeCleanedUp = (version2) => projectPredatesRunOnValueChange(version2);
276395
- var incomingOf = (connections, id) => JSON.stringify(
276396
- (connections ?? []).filter((c) => c.toId === id).map((c) => `${c.fromId}.${c.fromProperty}>${c.toProperty}`).sort()
276397
- );
276398
- function pinAuthoredRunOnChange(key2, files, onDisk) {
276399
- const nodes = files.nodes.nodes ?? [];
276400
- const plan = planRunOnValueChangeMigration({
276401
- components: [{ name: key2, graph: { roots: nodes, connections: files.connections.connections } }]
276402
- });
276403
- if (plan.writes.length === 0) return [];
276404
- const before = /* @__PURE__ */ new Map();
276405
- for (const n of onDisk?.nodes.nodes ?? []) before.set(n.id, n);
276406
- const authored = (n) => {
276407
- const old = before.get(n.id);
276408
- if (!old) return true;
276409
- if (JSON.stringify(old) !== JSON.stringify(n)) return true;
276410
- return incomingOf(onDisk?.connections.connections, n.id) !== incomingOf(files.connections.connections, n.id);
276411
- };
276412
- const byNode = /* @__PURE__ */ new Map();
276413
- for (const w of plan.writes) byNode.set(w.nodeId, [...byNode.get(w.nodeId) ?? [], w.parameter]);
276414
- const written2 = [];
276415
- for (const node of nodes) {
276416
- const parameters = byNode.get(node.id);
276417
- if (!parameters || !authored(node)) continue;
276418
- const existing = node.parameters ?? {};
276419
- const rebuilt = {};
276420
- for (const p of parameters) rebuilt[p] = true;
276421
- for (const name of Object.keys(existing)) if (!(name in rebuilt)) rebuilt[name] = existing[name];
276422
- node.parameters = rebuilt;
276423
- for (const p of parameters) written2.push({ node: node.id, parameter: p });
276424
- }
276425
- return written2;
276426
- }
276427
278453
  var TICKED_WHY = 'This project was saved before the "run on value change" setting existed. The editor upgrades it the next time it opens. That upgrade unticks every unset box on a node whose Run is wired. The door wrote these boxes as ticked, which is what an unset box means today. Each node still runs when these values change. Send the parameter as false to switch one off.';
276428
278454
  function tickedPayload(boxes) {
276429
278455
  return boxes && boxes.length > 0 ? { ticked: { boxes: [...boxes], why: TICKED_WHY } } : {};
@@ -277629,6 +279655,70 @@ function completionPayload(state) {
277629
279655
  };
277630
279656
  }
277631
279657
 
279658
+ // src/honesty.ts
279659
+ var COMPONENT_SCRIPT_NODES_SHOWN = 12;
279660
+ var PROJECT_COMPONENTS_SHOWN = 5;
279661
+ var PROJECT_SCRIPT_TYPES_SHOWN = 8;
279662
+ function honestyComponents(store2) {
279663
+ return portProjectFromStore(store2.format).components.map((c) => ({ name: c.name, nodes: c.nodes }));
279664
+ }
279665
+ function projectHonestyOf(store2) {
279666
+ return projectHonesty(honestyComponents(store2), { coreTypes: shippedTypeNames() });
279667
+ }
279668
+ function componentHonestyOf(name, nodes) {
279669
+ return componentHonesty({ name, nodes }, { coreTypes: shippedTypeNames() });
279670
+ }
279671
+ function projectHonestyPayload(h) {
279672
+ const byType2 = {};
279673
+ for (const p of h.scriptParts) byType2[p.type] = (byType2[p.type] ?? 0) + 1;
279674
+ return {
279675
+ line: honestyLine(h, 0),
279676
+ percent: h.percent,
279677
+ known: h.known,
279678
+ script: h.script,
279679
+ unspecced: h.unspecced,
279680
+ percentByLines: h.percentByLines,
279681
+ scriptLines: h.scriptLines,
279682
+ scriptByType: Object.fromEntries(
279683
+ Object.entries(byType2).sort((a8, b) => b[1] - a8[1] || a8[0].localeCompare(b[0])).slice(0, PROJECT_SCRIPT_TYPES_SHOWN)
279684
+ ),
279685
+ mostScript: h.components.filter((c) => c.script > 0).sort((a8, b) => b.script - a8.script || a8.component.localeCompare(b.component)).slice(0, PROJECT_COMPONENTS_SHOWN).map((c) => ({ component: c.component, percent: c.percent, script: c.script })),
279686
+ about: HONESTY_SENTENCE
279687
+ };
279688
+ }
279689
+ function componentHonestyPayload(h) {
279690
+ const parts = [...h.scriptParts].sort((a8, b) => b.lines - a8.lines);
279691
+ return {
279692
+ line: honestyLine(h),
279693
+ percent: h.percent,
279694
+ known: h.known,
279695
+ script: h.script,
279696
+ unspecced: h.unspecced,
279697
+ scriptNodes: parts.slice(0, COMPONENT_SCRIPT_NODES_SHOWN).map((p) => ({ id: p.id, type: p.type, ...p.label ? { label: p.label } : {}, lines: p.lines })),
279698
+ ...parts.length > COMPONENT_SCRIPT_NODES_SHOWN ? { moreScriptNodes: parts.length - COMPONENT_SCRIPT_NODES_SHOWN } : {}
279699
+ };
279700
+ }
279701
+ function writeHonestyPayload(store2, legacyName) {
279702
+ const project2 = projectHonestyOf(store2);
279703
+ const mine = project2.components.find((c) => c.component === legacyName);
279704
+ return {
279705
+ honesty: {
279706
+ component: mine ? `this component: ${honestyLine(mine)}` : "this component: not found after the write",
279707
+ project: `the project: ${honestyLine(project2, 0)}`
279708
+ }
279709
+ };
279710
+ }
279711
+ function honestySection(h) {
279712
+ const most = projectHonestyPayload(h).mostScript;
279713
+ const named = h.scriptParts.length ? [...h.scriptParts].sort((a8, b) => b.lines - a8.lines).slice(0, 3).map((p) => "`" + scriptPartName(p) + "`") : [];
279714
+ return [
279715
+ `HONESTY \u2014 ${honestyLine(h, 0)}` + (h.percentByLines !== null && h.script ? `; ${h.percentByLines}% weighed by script lines (${h.scriptLines} lines).` : "."),
279716
+ most.length ? `Most script: ${most.map((c) => `${c.component} ${c.script} (${c.percent === null ? "\u2014" : c.percent + "%"})`).join(", ")}.` : "No script nodes.",
279717
+ named.length ? `Largest script parts: ${named.join(", ")}.` : "",
279718
+ HONESTY_SENTENCE
279719
+ ].filter(Boolean).join("\n");
279720
+ }
279721
+
277632
279722
  // src/tools/util.ts
277633
279723
  function jsonResult(payload) {
277634
279724
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
@@ -279108,7 +281198,9 @@ function registerAuthorTools(server, binding, plans, examples, ledger, disclosur
279108
281198
  // it yet. Said on every visual write rather than only on pages: a
279109
281199
  // section component is what the page is made of, and "I only changed
279110
281200
  // a card" is how a page stops being the thing anybody renders.
279111
- ...drawsSomething(candidate) ? completionPayload(ledger.state(store2.projectDir)) : {}
281201
+ ...drawsSomething(candidate) ? completionPayload(ledger.state(store2.projectDir)) : {},
281202
+ // P111 SEE-001 — the component's number with its script named, and the project's after this write
281203
+ ...writeHonestyPayload(store2, legacyName)
279112
281204
  };
279113
281205
  return jsonResult(payload);
279114
281206
  }
@@ -279202,7 +281294,9 @@ function registerAuthorTools(server, binding, plans, examples, ledger, disclosur
279202
281294
  ...backendRevealPayload(disclosure?.revealForNodes(candidate.nodes.nodes) ?? []),
279203
281295
  ...unchanged ? {} : cloudDeployPayload([stored.legacyName], store2, disclosure),
279204
281296
  ...visualRootsPayload(candidate, args.set?.visual_roots),
279205
- ...drawsSomething(candidate) ? completionPayload(ledger.state(store2.projectDir)) : {}
281297
+ ...drawsSomething(candidate) ? completionPayload(ledger.state(store2.projectDir)) : {},
281298
+ // P111 SEE-001 — the component's number with its script named, and the project's after this write
281299
+ ...writeHonestyPayload(store2, stored.legacyName)
279206
281300
  };
279207
281301
  return jsonResult(payload);
279208
281302
  }
@@ -281914,7 +284008,7 @@ function registerBackendWriteTools(server, binding) {
281914
284008
  }).optional().describe('Required for type "schedule"'),
281915
284009
  webhook: external_exports.object({
281916
284010
  slug: external_exports.string().describe("URL slug: POST /hooks/<backendId>/<slug>"),
281917
- scheme: external_exports.enum(["hmac-sha256", "token"]).optional().describe("Secret scheme (default hmac-sha256)"),
284011
+ scheme: external_exports.enum(["hmac-sha256", "hmac-sha512", "token"]).optional().describe("Secret scheme (default hmac-sha256; hmac-sha512 is Paystack: secret = its key)"),
281918
284012
  maxBodyBytes: external_exports.number().optional().describe("Body size limit (default 1MB)")
281919
284013
  }).optional().describe('Required for type "webhook"'),
281920
284014
  dbChange: external_exports.object({
@@ -287007,7 +289101,9 @@ function registerReadTools(server, binding, options) {
287007
289101
  note: "Standing preferences this user wrote about themselves. They hold in every project, and they outrank your own habits and defaults. This project's own conventions (docs/CONVENTIONS.md) outrank them in turn: where the two disagree, follow the project and say that you did.",
287008
289102
  preferences: userPreferences
287009
289103
  }
287010
- } : {}
289104
+ } : {},
289105
+ // P111 SEE-001 — the honesty number, always (R2). A result field: no resident-surface cost.
289106
+ honesty: projectHonestyPayload(projectHonestyOf(store2))
287011
289107
  };
287012
289108
  return jsonResult(payload);
287013
289109
  })
@@ -287081,6 +289177,7 @@ function registerReadTools(server, binding, options) {
287081
289177
  };
287082
289178
  const drawn3 = drawnPortsFor(store2, stored.files.component.path ?? "/" + stored.key, stored.files.nodes.nodes);
287083
289179
  if (Object.keys(drawn3).length) payload.drawnPorts = drawn3;
289180
+ payload.honesty = componentHonestyPayload(componentHonestyOf(stored.legacyName, stored.files.nodes.nodes));
287084
289181
  return jsonResult(payload);
287085
289182
  })
287086
289183
  );
@@ -287210,6 +289307,9 @@ function registerRenderTools(server, binding, ledger, options = {}) {
287210
289307
  // page nobody could reach.
287211
289308
  ...verdict ? [{ type: "text", text: verdictHeadline(verdict) }] : [],
287212
289309
  { type: "text", text: JSON.stringify(report3) },
289310
+ // P111 SEE-001 — the honesty number, the same one get_project_info and every write report (a result block:
289311
+ // no resident-surface cost). Read from the files the render read.
289312
+ { type: "text", text: honestySection(projectHonestyOf(binding.require())) },
287213
289313
  // FLD-011 AC1 — with `out_dir` the harness wrote files and returned no base64, so
287214
289314
  // `screenshots` is empty and the loop below produces nothing. The manifest replaces it,
287215
289315
  // because a caller told only "written to a folder" has to list a directory to find out
@@ -291275,7 +293375,7 @@ function refuseUnknownArguments(server) {
291275
293375
  // src/server.ts
291276
293376
  var PKG_VERSION3 = (
291277
293377
  // eslint-disable-next-line @typescript-eslint/no-var-requires
291278
- true ? "0.4.1" : null.version
293378
+ true ? "0.4.2" : null.version
291279
293379
  );
291280
293380
  function createServer2(options) {
291281
293381
  setClientProfile(options.profile);