@aglyn/plugins-ai 1.0.0-beta.238 → 1.0.0-beta.240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.js +8 -0
  3. package/src/lib/components/ai-job-links.js.map +1 -1
  4. package/src/lib/components/ai-job-requests.d.ts +5 -0
  5. package/src/lib/components/ai-job-requests.js +4 -2
  6. package/src/lib/components/ai-job-requests.js.map +1 -1
  7. package/src/lib/components/ai-jobs-store.d.ts +12 -0
  8. package/src/lib/components/ai-jobs-store.js +15 -0
  9. package/src/lib/components/ai-jobs-store.js.map +1 -1
  10. package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
  11. package/src/lib/components/ai-usage-meter-refresh.js +63 -0
  12. package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
  13. package/src/lib/components/assist-panel.component.js +17 -1
  14. package/src/lib/components/assist-panel.component.js.map +1 -1
  15. package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
  16. package/src/lib/components/use-ai-usage-meter.js +8 -3
  17. package/src/lib/components/use-ai-usage-meter.js.map +1 -1
  18. package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
  19. package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
  20. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  21. package/src/lib/jobs/ai-job-build-step.js +29 -2
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-draft-ids.js +7 -2
  24. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  25. package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
  26. package/src/lib/jobs/ai-job-form-dataset.js +161 -0
  27. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
  28. package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
  29. package/src/lib/jobs/ai-job-form-step.js +37 -9
  30. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  31. package/src/lib/jobs/ai-job-layout-language.js +7 -2
  32. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  33. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  34. package/src/lib/jobs/ai-job-page-language.js +44 -4
  35. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  36. package/src/lib/jobs/ai-job-page-sections.js +13 -2
  37. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-step.js +32 -2
  39. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-plan-step.d.ts +18 -0
  41. package/src/lib/jobs/ai-job-plan-step.js +41 -4
  42. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  43. package/src/lib/jobs/ai-job-site-datasets.d.ts +183 -0
  44. package/src/lib/jobs/ai-job-site-datasets.js +530 -0
  45. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
  46. package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
  47. package/src/lib/jobs/ai-job-site-step.js +148 -29
  48. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  49. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
  50. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
  51. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
  52. package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -0
  53. package/src/lib/layout-language/ai-layout-compiler.js +93 -1
  54. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  55. package/src/lib/layout-language/ai-layout-gaps.js +17 -1
  56. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  57. package/src/lib/layout-language/ai-layout-listings.d.ts +32 -3
  58. package/src/lib/layout-language/ai-layout-listings.js +62 -9
  59. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  60. package/src/lib/layout-language/ai-layout-pictures.js +12 -4
  61. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  62. package/src/lib/model/ai-build-job.d.ts +2 -0
  63. package/src/lib/model/ai-build-job.js +16 -1
  64. package/src/lib/model/ai-build-job.js.map +1 -1
  65. package/src/lib/model/ai-build-plan.d.ts +23 -3
  66. package/src/lib/model/ai-build-plan.js +47 -6
  67. package/src/lib/model/ai-build-plan.js.map +1 -1
  68. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  69. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  70. package/src/lib/model/ai-site-job.d.ts +19 -2
  71. package/src/lib/model/ai-site-job.js +21 -6
  72. package/src/lib/model/ai-site-job.js.map +1 -1
  73. package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
  74. package/src/lib/runtime/ai-dataset-generation.js +127 -0
  75. package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
  76. package/src/lib/runtime/ai-doctrine-validators.d.ts +84 -0
  77. package/src/lib/runtime/ai-doctrine-validators.js +307 -1
  78. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  79. package/src/lib/runtime/ai-doctrine.js +10 -4
  80. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  81. package/src/lib/runtime/ai-node-tree.d.ts +2 -0
  82. package/src/lib/runtime/ai-node-tree.js +16 -0
  83. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  84. package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
  85. package/src/lib/tools/ai-dataset-tool.js +231 -0
  86. package/src/lib/tools/ai-dataset-tool.js.map +1 -0
  87. package/src/lib/usage/free-ai-credits-left.js +26 -2
  88. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -0,0 +1,161 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
18
+ import { collectFormFieldNodeIds, FORM_COMPONENT_ID, isMarketingConsentFieldName } from "@aglyn/aglyn/app-utils/forms";
19
+ import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
20
+ import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
21
+ /**
22
+ * A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's
23
+ * forms can write to datasets — a volunteer sign-up into a Volunteers
24
+ * dataset, an RSVP into an event's attendees, a catering inquiry into
25
+ * Inquiries).
26
+ *
27
+ * The plan says which dataset a form writes to (`writesTo` on its creation);
28
+ * it is never inferred from what a section places. The dataset is built
29
+ * first — a site start's dataset unit, a build's dataset item, or one the
30
+ * site already has — and the form step binds the form it generated the way a
31
+ * person binds one on the Form element: the form node's `datasetId`, and
32
+ * each Form Field's `datasetFieldId` where the dataset has a field of the
33
+ * same name and a type the submitted value can be stored as. A field the
34
+ * dataset lacks is left unbound, as the Form element's mapping leaves one:
35
+ * its value stays in the Inbox copy, and a form never adds a field to a
36
+ * dataset.
37
+ *
38
+ * The gates are the platform's. The binding is made only where the plan
39
+ * includes the data store (`dataStore`), the data plugin's writer is loaded
40
+ * here and reads the dataset, and the dataset is one this job made or one the
41
+ * site's inventory shows. Records, their allowance and storage are held at
42
+ * each submission by the data plugin's own record target. Anywhere else the
43
+ * form is the form it always was, its submissions in the Inbox.
44
+ */ /** The unit input a dataset this job made for the form rides in, by id. */ export const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade';
45
+ /** A field name as two spellings of it compare: `fullName`, `full_name` and "Full name" are one. */ export function aiFormFieldKey(name) {
46
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().replace(/[^a-z0-9]+/g, '');
47
+ }
48
+ const NUMBER_TYPES = new Set([
49
+ 'float',
50
+ 'int32',
51
+ 'int64',
52
+ 'number',
53
+ 'integer'
54
+ ]);
55
+ /**
56
+ * Whether a submitted value of this form field type can be stored in a
57
+ * dataset field of this storage type. Every value a form submits is text, so
58
+ * a text field takes any of them and a number field takes a rating; any
59
+ * other pairing is left unbound rather than refused at every submission.
60
+ */ export function aiFormFieldFitsDataset(fieldType, datasetType) {
61
+ if (datasetType === 'text') return true;
62
+ if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating';
63
+ return false;
64
+ }
65
+ /**
66
+ * Binds a form design to a dataset, in place: the form node's `datasetId`,
67
+ * and `datasetFieldId` on each Form Field the dataset has a field for, by its
68
+ * `fieldName` or its label, each dataset field taken once. The platform's
69
+ * consent field is never bound. A binding the design already carried is
70
+ * replaced, so what the model drew never chooses where a record lands.
71
+ */ export function aiBindFormToDataset(nodes, formNodeId, dataset) {
72
+ var _form_props;
73
+ const result = {
74
+ bound: [],
75
+ unbound: []
76
+ };
77
+ const form = nodes[formNodeId];
78
+ if ((form == null ? void 0 : form.componentId) !== FORM_COMPONENT_ID) return result;
79
+ const props = _extends({}, (_form_props = form.props) != null ? _form_props : {});
80
+ delete props['datasetName'];
81
+ props['datasetId'] = dataset.id;
82
+ form.props = props;
83
+ const taken = new Set();
84
+ for (const id of collectFormFieldNodeIds(nodes, formNodeId)){
85
+ var _field_props, _fieldProps_fieldName, _fieldProps_label, _fieldProps_fieldType, _matching;
86
+ const field = nodes[id];
87
+ if (!field) continue;
88
+ const fieldProps = _extends({}, (_field_props = field.props) != null ? _field_props : {});
89
+ delete fieldProps['datasetFieldId'];
90
+ field.props = fieldProps;
91
+ const fieldName = String((_fieldProps_fieldName = fieldProps['fieldName']) != null ? _fieldProps_fieldName : '').trim();
92
+ if (!fieldName || isMarketingConsentFieldName(fieldName)) continue;
93
+ const label = String((_fieldProps_label = fieldProps['label']) != null ? _fieldProps_label : '').trim();
94
+ const fieldType = String((_fieldProps_fieldType = fieldProps['fieldType']) != null ? _fieldProps_fieldType : 'text');
95
+ // Its name first, then its label: `fullName` is "Full name" before "Your name" is.
96
+ const matching = (key)=>key ? dataset.fields.find((one)=>!taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type)) : undefined;
97
+ const match = (_matching = matching(aiFormFieldKey(fieldName))) != null ? _matching : matching(aiFormFieldKey(label));
98
+ if (!match) {
99
+ result.unbound.push(label || fieldName);
100
+ continue;
101
+ }
102
+ taken.add(match.id);
103
+ fieldProps['datasetFieldId'] = match.id;
104
+ result.bound.push(fieldName);
105
+ }
106
+ return result;
107
+ }
108
+ const str = (value)=>typeof value === 'string' ? value.trim() : '';
109
+ /**
110
+ * The dataset a form may write to, or `null` where it may not: the plan
111
+ * includes no data store, the data plugin's writer is not loaded here, the
112
+ * dataset is neither one this job made nor one the site's inventory shows,
113
+ * or it has no field a submission could fill.
114
+ */ export async function aiReadFormDataset(input, deps = {}) {
115
+ var _ref, _deps_writerFor, _record_facts;
116
+ var _input_inventory;
117
+ const id = str(input.id);
118
+ if (!id) return null;
119
+ if (!checkEntitlement(input.org, 'dataStore')) return null;
120
+ const known = id === str(input.madeId) || ((_ref = (_input_inventory = input.inventory) == null ? void 0 : _input_inventory.datasets) != null ? _ref : []).some((row)=>row.id === id);
121
+ if (!known) return null;
122
+ const keeper = ((_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE);
123
+ if (!keeper) return null;
124
+ const record = await keeper.writer.read({
125
+ hostId: input.hostId,
126
+ id
127
+ }).catch((error)=>{
128
+ console.error('ai form: the dataset it writes to could not be read', {
129
+ hostId: input.hostId,
130
+ id,
131
+ error
132
+ });
133
+ return null;
134
+ });
135
+ if (!record) return null;
136
+ const facts = (_record_facts = record.facts) != null ? _record_facts : {};
137
+ const address = str(facts.addressField);
138
+ const fields = (Array.isArray(facts.fields) ? facts.fields : []).map((raw)=>raw && typeof raw === 'object' ? raw : {}).map((raw)=>({
139
+ id: str(raw['id']),
140
+ name: str(raw['name']),
141
+ type: str(raw['type']) || 'text'
142
+ })).filter((field)=>field.id && field.name && field.id !== address);
143
+ if (!fields.length) return null;
144
+ return {
145
+ id,
146
+ name: str(record.name) || 'dataset',
147
+ fields
148
+ };
149
+ }
150
+ /** What the form's generation is told about the dataset it writes to. */ export function aiFormDatasetPromptLine(dataset) {
151
+ const fields = dataset.fields.map((field)=>`${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`);
152
+ return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`;
153
+ }
154
+ /** What the form's row says about where its submissions are also saved. */ export function aiFormDatasetNote(dataset, binding) {
155
+ const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`;
156
+ if (!binding.unbound.length) return kept;
157
+ const one = binding.unbound.length === 1;
158
+ return `${kept} The dataset has no field for ${binding.unbound.map((name)=>`“${name}”`).join(', ')}, so ${one ? 'that answer stays' : 'those answers stay'} in the Inbox only.`;
159
+ }
160
+
161
+ //# sourceMappingURL=ai-job-form-dataset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-dataset.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n isMarketingConsentFieldName,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_DATASET_DRAFT_RESOURCE } from './ai-build-unit-outcome'\n\n/**\n * A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's\n * forms can write to datasets — a volunteer sign-up into a Volunteers\n * dataset, an RSVP into an event's attendees, a catering inquiry into\n * Inquiries).\n *\n * The plan says which dataset a form writes to (`writesTo` on its creation);\n * it is never inferred from what a section places. The dataset is built\n * first — a site start's dataset unit, a build's dataset item, or one the\n * site already has — and the form step binds the form it generated the way a\n * person binds one on the Form element: the form node's `datasetId`, and\n * each Form Field's `datasetFieldId` where the dataset has a field of the\n * same name and a type the submitted value can be stored as. A field the\n * dataset lacks is left unbound, as the Form element's mapping leaves one:\n * its value stays in the Inbox copy, and a form never adds a field to a\n * dataset.\n *\n * The gates are the platform's. The binding is made only where the plan\n * includes the data store (`dataStore`), the data plugin's writer is loaded\n * here and reads the dataset, and the dataset is one this job made or one the\n * site's inventory shows. Records, their allowance and storage are held at\n * each submission by the data plugin's own record target. Anywhere else the\n * form is the form it always was, its submissions in the Inbox.\n */\n\n/** The unit input a dataset this job made for the form rides in, by id. */\nexport const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade'\n\n/** One field of the dataset a form writes to, as its writer reports it: storage type words. */\nexport interface AiFormDatasetField {\n id: string\n name: string\n type: string\n}\n\n/** The dataset a form writes to, with the fields a submission may fill. */\nexport interface AiFormDataset {\n id: string\n name: string\n fields: AiFormDatasetField[]\n}\n\n/** A field name as two spellings of it compare: `fullName`, `full_name` and \"Full name\" are one. */\nexport function aiFormFieldKey(name: string): string {\n return name\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '')\n}\n\nconst NUMBER_TYPES = new Set(['float', 'int32', 'int64', 'number', 'integer'])\n\n/**\n * Whether a submitted value of this form field type can be stored in a\n * dataset field of this storage type. Every value a form submits is text, so\n * a text field takes any of them and a number field takes a rating; any\n * other pairing is left unbound rather than refused at every submission.\n */\nexport function aiFormFieldFitsDataset(fieldType: string, datasetType: string): boolean {\n if (datasetType === 'text') return true\n if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating'\n return false\n}\n\n/** What a binding came to: the fields bound, and the fields the dataset has no place for. */\nexport interface AiFormDatasetBinding {\n bound: string[]\n unbound: string[]\n}\n\n/**\n * Binds a form design to a dataset, in place: the form node's `datasetId`,\n * and `datasetFieldId` on each Form Field the dataset has a field for, by its\n * `fieldName` or its label, each dataset field taken once. The platform's\n * consent field is never bound. A binding the design already carried is\n * replaced, so what the model drew never chooses where a record lands.\n */\nexport function aiBindFormToDataset(\n nodes: NodesMap,\n formNodeId: string,\n dataset: AiFormDataset,\n): AiFormDatasetBinding {\n const result: AiFormDatasetBinding = { bound: [], unbound: [] }\n const form = nodes[formNodeId]\n if (form?.componentId !== FORM_COMPONENT_ID) return result\n const props = { ...((form.props ?? {}) as Record<string, unknown>) }\n delete props['datasetName']\n props['datasetId'] = dataset.id\n form.props = props as never\n const taken = new Set<string>()\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const field = nodes[id]\n if (!field) continue\n const fieldProps = { ...((field.props ?? {}) as Record<string, unknown>) }\n delete fieldProps['datasetFieldId']\n field.props = fieldProps as never\n const fieldName = String(fieldProps['fieldName'] ?? '').trim()\n if (!fieldName || isMarketingConsentFieldName(fieldName)) continue\n const label = String(fieldProps['label'] ?? '').trim()\n const fieldType = String(fieldProps['fieldType'] ?? 'text')\n // Its name first, then its label: `fullName` is \"Full name\" before \"Your name\" is.\n const matching = (key: string) =>\n key\n ? dataset.fields.find((one) => !taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type))\n : undefined\n const match = matching(aiFormFieldKey(fieldName)) ?? matching(aiFormFieldKey(label))\n if (!match) {\n result.unbound.push(label || fieldName)\n continue\n }\n taken.add(match.id)\n fieldProps['datasetFieldId'] = match.id\n result.bound.push(fieldName)\n }\n return result\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nexport interface AiFormDatasetReadDeps {\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/**\n * The dataset a form may write to, or `null` where it may not: the plan\n * includes no data store, the data plugin's writer is not loaded here, the\n * dataset is neither one this job made nor one the site's inventory shows,\n * or it has no field a submission could fill.\n */\nexport async function aiReadFormDataset(\n input: {\n hostId: string\n id: string | null | undefined\n org: object | null\n inventory: Pick<AiSiteInventory, 'datasets'> | null\n /** The dataset this job made for the form, by id. */\n madeId?: unknown\n },\n deps: AiFormDatasetReadDeps = {},\n): Promise<AiFormDataset | null> {\n const id = str(input.id)\n if (!id) return null\n if (!checkEntitlement(input.org as never, 'dataStore')) return null\n const known = id === str(input.madeId) || (input.inventory?.datasets ?? []).some((row) => row.id === id)\n if (!known) return null\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE)\n if (!keeper) return null\n const record = await keeper.writer.read({ hostId: input.hostId, id }).catch((error: unknown) => {\n console.error('ai form: the dataset it writes to could not be read', { hostId: input.hostId, id, error })\n return null\n })\n if (!record) return null\n const facts = (record.facts ?? {}) as { fields?: unknown; addressField?: unknown }\n const address = str(facts.addressField)\n const fields = (Array.isArray(facts.fields) ? (facts.fields as unknown[]) : [])\n .map((raw) => (raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}))\n .map((raw) => ({ id: str(raw['id']), name: str(raw['name']), type: str(raw['type']) || 'text' }))\n .filter((field) => field.id && field.name && field.id !== address)\n if (!fields.length) return null\n return { id, name: str(record.name) || 'dataset', fields }\n}\n\n/** What the form's generation is told about the dataset it writes to. */\nexport function aiFormDatasetPromptLine(dataset: AiFormDataset): string {\n const fields = dataset.fields.map((field) => `${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`)\n return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`\n}\n\n/** What the form's row says about where its submissions are also saved. */\nexport function aiFormDatasetNote(dataset: AiFormDataset, binding: AiFormDatasetBinding): string {\n const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`\n if (!binding.unbound.length) return kept\n const one = binding.unbound.length === 1\n return `${kept} The dataset has no field for ${binding.unbound.map((name) => `“${name}”`).join(', ')}, so ${\n one ? 'that answer stays' : 'those answers stay'\n } in the Inbox only.`\n}\n"],"names":["checkEntitlement","collectFormFieldNodeIds","FORM_COMPONENT_ID","isMarketingConsentFieldName","pluginResourceDraftWriter","AI_DATASET_DRAFT_RESOURCE","AI_FORM_DATASET_MADE_INPUT","aiFormFieldKey","name","replace","toLowerCase","NUMBER_TYPES","Set","aiFormFieldFitsDataset","fieldType","datasetType","has","aiBindFormToDataset","nodes","formNodeId","dataset","form","result","bound","unbound","componentId","props","id","taken","field","fieldProps","matching","fieldName","String","trim","label","key","fields","find","one","type","undefined","match","push","add","str","value","aiReadFormDataset","input","deps","record","org","known","madeId","inventory","datasets","some","row","keeper","writerFor","writer","read","hostId","catch","error","console","facts","address","addressField","Array","isArray","map","raw","filter","length","aiFormDatasetPromptLine","join","aiFormDatasetNote","binding","kept"],"mappings":"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"}
@@ -24,6 +24,7 @@ import type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
24
24
  import type { AiSystemBlock, AiTool } from '../runtime/ai-runtime';
25
25
  import { readSiteInventory } from '../runtime/site-inventory';
26
26
  import { type AiJobAdmission } from './ai-job-admission';
27
+ import { type AiFormDataset, type AiFormDatasetBinding, type AiFormDatasetReadDeps } from './ai-job-form-dataset';
27
28
  import type { AiJobStepRunner } from './ai-job-text-step';
28
29
  /**
29
30
  * The form step (AGL-2913): the generation step of a `form` job, run once a
@@ -140,18 +141,26 @@ export interface AiFormDraft {
140
141
  consentFieldName: string | null;
141
142
  routing: FormRouting | null;
142
143
  answer: AiFormAnswer;
144
+ /** The dataset its submissions are also written to, and what was bound (AGL-3616). */
145
+ dataset?: {
146
+ dataset: AiFormDataset;
147
+ binding: AiFormDatasetBinding;
148
+ };
143
149
  }
144
150
  /**
145
151
  * The draft a validated design and its answer make: the form node bound to
146
- * `formId` and captioned `name`, no dataset binding, the platform's consent
147
- * field when the fields can yield an email address, the canvas root above,
148
- * and the declaration read off the result. Pure; the input tree is not
152
+ * `formId` and captioned `name`, the platform's consent field when the
153
+ * fields can yield an email address, the canvas root above, and the
154
+ * declaration read off the result. A dataset binding is made only where the
155
+ * plan said the form writes to one this step may bind (`identity.dataset`,
156
+ * AGL-3616); any the model drew is dropped. Pure; the input tree is not
149
157
  * changed.
150
158
  */
151
159
  export declare function aiFormDraft(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>, answer: Record<string, unknown>, identity: {
152
160
  formId: string;
153
161
  name: string;
154
162
  decisions?: AiFormDecisions;
163
+ dataset?: AiFormDataset | null;
155
164
  }): AiFormDraft;
156
165
  /**
157
166
  * What a draft would break once published, as building-rule violations
@@ -170,6 +179,7 @@ export declare function aiFormDraftCheck(identity: {
170
179
  formId: string;
171
180
  name: string;
172
181
  decisions?: AiFormDecisions;
182
+ dataset?: AiFormDataset | null;
173
183
  }): {
174
184
  extend: (tree: AiValidatedTree, answer: Record<string, unknown>) => AiDoctrineViolation[];
175
185
  draftFor: (tree: AiValidatedTree) => AiFormDraft | undefined;
@@ -280,6 +290,8 @@ export interface AiJobFormStepDeps {
280
290
  readInventory?: typeof readSiteInventory;
281
291
  /** The platform's duplicate module, for a plan that starts from a copy. */
282
292
  duplicate?: typeof duplicateResource;
293
+ /** The data plugin's writer lookup the form's dataset is read through (AGL-3616); specs hand in a fake. */
294
+ datasetWriterFor?: AiFormDatasetReadDeps['writerFor'];
283
295
  }
284
296
  export declare function createAiJobFormStep(deps?: AiJobFormStepDeps): AiJobStepRunner;
285
297
  export declare const runAiJobFormStep: AiJobStepRunner;
@@ -28,6 +28,7 @@ import { aiDoctrineTreeTool, runValidatedGeneration } from "../runtime/ai-doctri
28
28
  import { AI_PALETTE } from "../runtime/ai-palette.generated.js";
29
29
  import { readSiteInventory } from "../runtime/site-inventory.js";
30
30
  import { registerAiJobAdmission } from "./ai-job-admission.js";
31
+ import { AI_FORM_DATASET_MADE_INPUT, aiBindFormToDataset, aiFormDatasetNote, aiFormDatasetPromptLine, aiReadFormDataset } from "./ai-job-form-dataset.js";
31
32
  import { aiJobPlanCreationsRefusal } from "./ai-job-plan-creations.js";
32
33
  import { aiJobDraftId } from "./ai-job-draft-ids.js";
33
34
  import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, writeAiDraft } from "./ai-job-drafts.js";
@@ -209,9 +210,11 @@ function oneLine(value, max) {
209
210
  }
210
211
  /**
211
212
  * The draft a validated design and its answer make: the form node bound to
212
- * `formId` and captioned `name`, no dataset binding, the platform's consent
213
- * field when the fields can yield an email address, the canvas root above,
214
- * and the declaration read off the result. Pure; the input tree is not
213
+ * `formId` and captioned `name`, the platform's consent field when the
214
+ * fields can yield an email address, the canvas root above, and the
215
+ * declaration read off the result. A dataset binding is made only where the
216
+ * plan said the form writes to one this step may bind (`identity.dataset`,
217
+ * AGL-3616); any the model drew is dropped. Pure; the input tree is not
215
218
  * changed.
216
219
  */ export function aiFormDraft(tree, answer, identity) {
217
220
  var _identity_decisions;
@@ -233,8 +236,8 @@ function oneLine(value, max) {
233
236
  let consentFieldName = null;
234
237
  if ((form == null ? void 0 : form.componentId) === FORM_COMPONENT_ID) {
235
238
  const props = form.props;
236
- // A dataset binding decides where every submission is also written; it
237
- // stays the person's choice, made on the Form element.
239
+ // A dataset binding decides where every submission is also written: the
240
+ // plan's to say (AGL-3616), bound below, never the model's to draw.
238
241
  delete props['datasetId'];
239
242
  delete props['datasetName'];
240
243
  props[FORM_ID_PROP] = identity.formId;
@@ -269,6 +272,12 @@ function oneLine(value, max) {
269
272
  }
270
273
  form.parentId = CANVAS_ROOT_ELEMENT_ID;
271
274
  }
275
+ // The dataset the plan says the form writes to (AGL-3616), bound as the
276
+ // Form element binds one.
277
+ const dataset = identity.dataset && (form == null ? void 0 : form.componentId) === FORM_COMPONENT_ID ? {
278
+ dataset: identity.dataset,
279
+ binding: aiBindFormToDataset(nodes, formNodeId, identity.dataset)
280
+ } : undefined;
272
281
  nodes[CANVAS_ROOT_ELEMENT_ID] = {
273
282
  $id: CANVAS_ROOT_ELEMENT_ID,
274
283
  componentId: 'div',
@@ -276,7 +285,7 @@ function oneLine(value, max) {
276
285
  formNodeId
277
286
  ]
278
287
  };
279
- return {
288
+ return _extends({
280
289
  nodes,
281
290
  rootId: CANVAS_ROOT_ELEMENT_ID,
282
291
  formNodeId,
@@ -286,7 +295,9 @@ function oneLine(value, max) {
286
295
  lead: true
287
296
  } : null,
288
297
  answer: parsed
289
- };
298
+ }, dataset ? {
299
+ dataset
300
+ } : {});
290
301
  }
291
302
  /**
292
303
  * What a draft would break once published, as building-rule violations
@@ -536,6 +547,7 @@ export function createAiJobFormStep(deps = {}) {
536
547
  const duplicate = (_deps_duplicate = deps.duplicate) != null ? _deps_duplicate : duplicateResource;
537
548
  return async ({ job, now, signal, firestore, modelFor })=>{
538
549
  var _ref, _orgSnapshot_data;
550
+ var _job_inputs;
539
551
  const hostId = job.hostId;
540
552
  if (!hostId) throw new Error('a form job names no site, and its admission refuses one');
541
553
  const output = (draft, extra = {})=>_extends({
@@ -662,10 +674,22 @@ export function createAiJobFormStep(deps = {}) {
662
674
  submissions: aiSiteSubmissions(job.inputs)
663
675
  };
664
676
  const { audience } = aiSiteWords(job.inputs);
677
+ // The dataset the plan says the form writes to (AGL-3616), where this
678
+ // workspace may bind one; otherwise the form is the form it always was.
679
+ const dataset = await aiReadFormDataset({
680
+ hostId,
681
+ id: creation == null ? void 0 : creation.writesTo,
682
+ org,
683
+ inventory,
684
+ madeId: (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_FORM_DATASET_MADE_INPUT]
685
+ }, deps.datasetWriterFor ? {
686
+ writerFor: deps.datasetWriterFor
687
+ } : {});
665
688
  const check = aiFormDraftCheck({
666
689
  formId: draftId,
667
690
  name,
668
- decisions
691
+ decisions,
692
+ dataset
669
693
  });
670
694
  const result = await runValidatedGeneration('form', _extends({
671
695
  step: 'job.form',
@@ -680,7 +704,10 @@ export function createAiJobFormStep(deps = {}) {
680
704
  audience,
681
705
  submissions: decisions.submissions
682
706
  }),
683
- ...sourceLines
707
+ ...sourceLines,
708
+ ...dataset ? [
709
+ aiFormDatasetPromptLine(dataset)
710
+ ] : []
684
711
  ].join('\n')
685
712
  }
686
713
  ],
@@ -728,6 +755,7 @@ export function createAiJobFormStep(deps = {}) {
728
755
  // What the person decides next, then the facts the brief did not give (AGL-3056).
729
756
  const note = [
730
757
  aiFormOutputNote(draft.answer),
758
+ draft.dataset ? aiFormDatasetNote(draft.dataset.dataset, draft.dataset.binding) : null,
731
759
  aiBracketedFactsNote({
732
760
  tree: {
733
761
  rootId: result.value.rootId,