@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.239
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
- package/src/lib/components/ai-credits-prompt.component.js +84 -34
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- package/src/lib/components/ai-job-links.js +8 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +12 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +12 -0
- package/src/lib/components/ai-jobs-store.js +15 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +22 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
- package/src/lib/components/ai-usage-meter-refresh.js +63 -0
- package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
- package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +12 -16
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +91 -51
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
- package/src/lib/components/use-ai-usage-meter.js +8 -3
- package/src/lib/components/use-ai-usage-meter.js.map +1 -1
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
- package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +33 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +7 -2
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
- package/src/lib/jobs/ai-job-form-dataset.js +161 -0
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
- package/src/lib/jobs/ai-job-form-step.js +41 -10
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
- package/src/lib/jobs/ai-job-page-language.js +45 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
- package/src/lib/jobs/ai-job-plan-step.js +90 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
- package/src/lib/jobs/ai-job-site-content.js +66 -7
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
- package/src/lib/jobs/ai-job-site-datasets.js +499 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
- package/src/lib/jobs/ai-job-site-step.js +152 -30
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +37 -12
- package/src/lib/jobs/ai-jobs.js +184 -28
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
- package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
- package/src/lib/layout-language/ai-layout-compiler.js +397 -9
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +17 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +44 -3
- package/src/lib/layout-language/ai-layout-listings.js +118 -50
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +11 -1
- package/src/lib/model/ai-build-job.js +35 -6
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +23 -3
- package/src/lib/model/ai-build-plan.js +47 -6
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +2 -2
- package/src/lib/model/ai-build-progress.js +6 -4
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +69 -2
- package/src/lib/model/ai-credit-estimate.js +91 -3
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +38 -4
- package/src/lib/model/ai-site-job.js +50 -14
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +34 -6
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
- package/src/lib/runtime/ai-dataset-generation.js +127 -0
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +77 -1
- package/src/lib/runtime/ai-doctrine-validators.js +280 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +10 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +2 -0
- package/src/lib/runtime/ai-node-tree.js +16 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +53 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
- package/src/lib/tools/ai-dataset-tool.js +231 -0
- package/src/lib/tools/ai-dataset-tool.js.map +1 -0
- package/src/lib/usage/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +30 -4
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { AI_ROUTING_TABLE } from "../providers/routing.js";
|
|
18
|
+
import { AI_DATASET_LIMITS, AI_DATASET_LIST_SEPARATOR, AI_DATASET_TOOL, AI_DATASET_TOOL_NAME, checkAiDataset } from "../tools/ai-dataset-tool.js";
|
|
19
|
+
import { runValidatedGeneration } from "./ai-doctrine.js";
|
|
20
|
+
/**
|
|
21
|
+
* A SITE'S DATASETS (AGL-3616): the typed fields and first records of one
|
|
22
|
+
* dataset a site plan created, through the doctrine's generation call and the
|
|
23
|
+
* strict `submit_dataset` tool. The data plugin's writer stores what it
|
|
24
|
+
* answers; nothing here writes.
|
|
25
|
+
*
|
|
26
|
+
* Routed as `job.products`, the row a store's first products are proposed on
|
|
27
|
+
* (no thinking, a structured list from a brief, held to the storefront claim
|
|
28
|
+
* rules): one dataset of a dozen records is the same work. Its answer is held
|
|
29
|
+
* to a ceiling of its own under that row's. The rules are cached and
|
|
30
|
+
* byte-identical; the site's words, the dataset and the pages that show it
|
|
31
|
+
* ride in the user turn.
|
|
32
|
+
*/ /** The step a dataset is routed as. */ export const AI_DATASET_STEP = 'job.products';
|
|
33
|
+
/**
|
|
34
|
+
* One dataset's answer ceiling: twelve fields and twelve records of a few
|
|
35
|
+
* sentences each, written as JSON, about 9,000 characters at three a token
|
|
36
|
+
* with room; under the routing row's.
|
|
37
|
+
*/ export const AI_DATASET_MAX_TOKENS = Math.min(4000, AI_ROUTING_TABLE[AI_DATASET_STEP].maxTokens);
|
|
38
|
+
/** The generation kind a dataset is asked under, which the doctrine scopes. */ export const AI_DATASET_KIND = 'dataset';
|
|
39
|
+
/** The rules for one dataset. Byte-identical on every request. */ export const AI_DATASET_INSTRUCTIONS = [
|
|
40
|
+
{
|
|
41
|
+
text: 'You design one dataset of a new website: a list of like things — a menu, a team, services, portfolio pieces, events, questions and answers — kept as records the owner edits in one place, which the site’s pages list.\n\n' + `Answer by calling ${AI_DATASET_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
|
|
42
|
+
'Keep the planned fields, in order and named exactly as planned; the pages around the dataset already name them. Add a field only where the records clearly need it.',
|
|
43
|
+
'Type each field: text for words, including a date or a time written the way a visitor reads it; number or integer only for a figure a visitor compares; boolean for yes or no; list for a few short tags. The first field names each record and is text.',
|
|
44
|
+
`Write ${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records, as many as the brief supports: what the business actually offers, in its own words where the brief gives them, otherwise plain and general enough to be true of it.`,
|
|
45
|
+
'Never invent a fact: no price, date, address, phone number, award, credential, client or number the brief does not give, and no review, testimonial, rating or quote from anyone. Leave a value "" rather than make one up.',
|
|
46
|
+
'Never give a person a name the brief does not give: a team member the brief does not name is their role, such as "Head chef".',
|
|
47
|
+
'Never say anything cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',
|
|
48
|
+
`Values are plain text with no links, markup, emoji or square brackets, at most ${AI_DATASET_LIMITS.valueMax} characters each; a description is one to three sentences.`,
|
|
49
|
+
'Write in the language of the brief.'
|
|
50
|
+
].map((rule)=>`- ${rule}`).join('\n'),
|
|
51
|
+
cacheBreakpoint: true
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
/** The user turn: the site's words, the dataset, and where it is shown. */ export function aiDatasetPrompt(input) {
|
|
55
|
+
return [
|
|
56
|
+
'Brief:',
|
|
57
|
+
input.brief.trim(),
|
|
58
|
+
`Dataset: “${input.name}” — ${input.why.trim() || 'the site’s structured content'}`,
|
|
59
|
+
`Planned fields: ${input.fields.length ? input.fields.map((field)=>`“${field}”`).join(', ') : '(none: choose them)'}`,
|
|
60
|
+
input.shownIn.length ? `Listed on: ${input.shownIn.join('; ')}.` : 'Listed on the site’s pages.',
|
|
61
|
+
input.recordPages ? 'Each record also gets a page of its own, so give each one enough to read: a sentence or two describing it.' : 'Each record is shown as a card, so keep its values short.'
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
/** Design one dataset. It writes nothing to the site. */ export function generateAiDataset(input) {
|
|
65
|
+
var _input_maxTokens;
|
|
66
|
+
const row = AI_ROUTING_TABLE[AI_DATASET_STEP];
|
|
67
|
+
return runValidatedGeneration(AI_DATASET_KIND, _extends({
|
|
68
|
+
step: AI_DATASET_STEP
|
|
69
|
+
}, input.settings ? {
|
|
70
|
+
settings: input.settings
|
|
71
|
+
} : {}, input.model ? {
|
|
72
|
+
model: input.model
|
|
73
|
+
} : {}, {
|
|
74
|
+
instructions: AI_DATASET_INSTRUCTIONS,
|
|
75
|
+
messages: [
|
|
76
|
+
{
|
|
77
|
+
role: 'user',
|
|
78
|
+
content: aiDatasetPrompt(input)
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
tool: AI_DATASET_TOOL,
|
|
82
|
+
maxTokens: (_input_maxTokens = input.maxTokens) != null ? _input_maxTokens : AI_DATASET_MAX_TOKENS
|
|
83
|
+
}, row.thinking ? {
|
|
84
|
+
thinking: row.thinking
|
|
85
|
+
} : {}, row.effort ? {
|
|
86
|
+
effort: row.effort
|
|
87
|
+
} : {}, input.signal ? {
|
|
88
|
+
signal: input.signal
|
|
89
|
+
} : {}, input.provider ? {
|
|
90
|
+
provider: input.provider
|
|
91
|
+
} : {}, {
|
|
92
|
+
check: (answer)=>checkAiDataset(answer, {
|
|
93
|
+
planned: input.fields,
|
|
94
|
+
merchantWords: input.merchantWords
|
|
95
|
+
})
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
/** A designed dataset as the data plugin's `dataset` writer takes it: values by field name. */ export function aiDatasetDraftContent(name, dataset, options) {
|
|
99
|
+
const records = dataset.records.map((values)=>{
|
|
100
|
+
const record = {};
|
|
101
|
+
dataset.fields.forEach((field, column)=>{
|
|
102
|
+
var _values_column;
|
|
103
|
+
const value = ((_values_column = values[column]) != null ? _values_column : '').trim();
|
|
104
|
+
if (!value) return;
|
|
105
|
+
if (field.type === 'list') {
|
|
106
|
+
record[field.name] = value.split(AI_DATASET_LIST_SEPARATOR.trim()).map((part)=>part.trim()).filter(Boolean);
|
|
107
|
+
} else if (field.type === 'number' || field.type === 'integer') {
|
|
108
|
+
record[field.name] = value.replace(/,/g, '');
|
|
109
|
+
} else {
|
|
110
|
+
record[field.name] = value;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return record;
|
|
114
|
+
});
|
|
115
|
+
return _extends({
|
|
116
|
+
name,
|
|
117
|
+
fields: dataset.fields.map((field)=>({
|
|
118
|
+
name: field.name,
|
|
119
|
+
type: field.type
|
|
120
|
+
})),
|
|
121
|
+
records
|
|
122
|
+
}, options.recordPages && dataset.fields[0] ? {
|
|
123
|
+
pageAddressFrom: dataset.fields[0].name
|
|
124
|
+
} : {});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=ai-dataset-generation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dataset-generation.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 type { AiStepKind } from '../providers/catalog'\nimport type { AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_DATASET_LIMITS,\n AI_DATASET_LIST_SEPARATOR,\n AI_DATASET_TOOL,\n AI_DATASET_TOOL_NAME,\n checkAiDataset,\n type AiDataset,\n} from '../tools/ai-dataset-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * A SITE'S DATASETS (AGL-3616): the typed fields and first records of one\n * dataset a site plan created, through the doctrine's generation call and the\n * strict `submit_dataset` tool. The data plugin's writer stores what it\n * answers; nothing here writes.\n *\n * Routed as `job.products`, the row a store's first products are proposed on\n * (no thinking, a structured list from a brief, held to the storefront claim\n * rules): one dataset of a dozen records is the same work. Its answer is held\n * to a ceiling of its own under that row's. The rules are cached and\n * byte-identical; the site's words, the dataset and the pages that show it\n * ride in the user turn.\n */\n\n/** The step a dataset is routed as. */\nexport const AI_DATASET_STEP: AiStepKind = 'job.products'\n\n/**\n * One dataset's answer ceiling: twelve fields and twelve records of a few\n * sentences each, written as JSON, about 9,000 characters at three a token\n * with room; under the routing row's.\n */\nexport const AI_DATASET_MAX_TOKENS = Math.min(4_000, AI_ROUTING_TABLE[AI_DATASET_STEP].maxTokens)\n\n/** The generation kind a dataset is asked under, which the doctrine scopes. */\nexport const AI_DATASET_KIND = 'dataset'\n\n/** The rules for one dataset. Byte-identical on every request. */\nexport const AI_DATASET_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You design one dataset of a new website: a list of like things — a menu, a team, services, portfolio pieces, events, questions and answers — kept as records the owner edits in one place, which the site’s pages list.\\n\\n' +\n `Answer by calling ${AI_DATASET_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Keep the planned fields, in order and named exactly as planned; the pages around the dataset already name them. Add a field only where the records clearly need it.',\n 'Type each field: text for words, including a date or a time written the way a visitor reads it; number or integer only for a figure a visitor compares; boolean for yes or no; list for a few short tags. The first field names each record and is text.',\n `Write ${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records, as many as the brief supports: what the business actually offers, in its own words where the brief gives them, otherwise plain and general enough to be true of it.`,\n 'Never invent a fact: no price, date, address, phone number, award, credential, client or number the brief does not give, and no review, testimonial, rating or quote from anyone. Leave a value \"\" rather than make one up.',\n 'Never give a person a name the brief does not give: a team member the brief does not name is their role, such as \"Head chef\".',\n 'Never say anything cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n `Values are plain text with no links, markup, emoji or square brackets, at most ${AI_DATASET_LIMITS.valueMax} characters each; a description is one to three sentences.`,\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiDatasetPromptInput {\n /** The site's words: the brief and its answers, as a unit's brief carries them. */\n brief: string\n /** The dataset as the plan named it, and why it has one. */\n name: string\n why: string\n /** The field names the plan gave it. */\n fields: readonly string[]\n /** The sections that list it, by page: \"Menu › Our dishes (12 items)\". */\n shownIn: readonly string[]\n /** Whether each record also gets a page of its own. */\n recordPages: boolean\n}\n\n/** The user turn: the site's words, the dataset, and where it is shown. */\nexport function aiDatasetPrompt(input: AiDatasetPromptInput): string {\n return [\n 'Brief:',\n input.brief.trim(),\n `Dataset: “${input.name}” — ${input.why.trim() || 'the site’s structured content'}`,\n `Planned fields: ${input.fields.length ? input.fields.map((field) => `“${field}”`).join(', ') : '(none: choose them)'}`,\n input.shownIn.length ? `Listed on: ${input.shownIn.join('; ')}.` : 'Listed on the site’s pages.',\n input.recordPages\n ? 'Each record also gets a page of its own, so give each one enough to read: a sentence or two describing it.'\n : 'Each record is shown as a card, so keep its values short.',\n ].join('\\n')\n}\n\nexport interface GenerateAiDatasetInput extends AiDatasetPromptInput {\n /** What a claim or a price may be quoted from: the brief and the site's answers. */\n merchantWords: string\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\n/** Design one dataset. It writes nothing to the site. */\nexport function generateAiDataset(input: GenerateAiDatasetInput): Promise<AiValidatedGeneration<AiDataset>> {\n const row = AI_ROUTING_TABLE[AI_DATASET_STEP]\n return runValidatedGeneration(AI_DATASET_KIND, {\n step: AI_DATASET_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_DATASET_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiDatasetPrompt(input) }],\n tool: AI_DATASET_TOOL,\n maxTokens: input.maxTokens ?? AI_DATASET_MAX_TOKENS,\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiDataset(answer, { planned: input.fields, merchantWords: input.merchantWords }),\n }) as Promise<AiValidatedGeneration<AiDataset>>\n}\n\n/** A designed dataset as the data plugin's `dataset` writer takes it: values by field name. */\nexport function aiDatasetDraftContent(\n name: string,\n dataset: AiDataset,\n options: { recordPages: boolean },\n): Record<string, unknown> {\n const records = dataset.records.map((values) => {\n const record: Record<string, unknown> = {}\n dataset.fields.forEach((field, column) => {\n const value = (values[column] ?? '').trim()\n if (!value) return\n if (field.type === 'list') {\n record[field.name] = value.split(AI_DATASET_LIST_SEPARATOR.trim()).map((part) => part.trim()).filter(Boolean)\n } else if (field.type === 'number' || field.type === 'integer') {\n record[field.name] = value.replace(/,/g, '')\n } else {\n record[field.name] = value\n }\n })\n return record\n })\n return {\n name,\n fields: dataset.fields.map((field) => ({ name: field.name, type: field.type })),\n records,\n // A record page shows each record at an address made from its name.\n ...(options.recordPages && dataset.fields[0] ? { pageAddressFrom: dataset.fields[0].name } : {}),\n }\n}\n"],"names":["AI_ROUTING_TABLE","AI_DATASET_LIMITS","AI_DATASET_LIST_SEPARATOR","AI_DATASET_TOOL","AI_DATASET_TOOL_NAME","checkAiDataset","runValidatedGeneration","AI_DATASET_STEP","AI_DATASET_MAX_TOKENS","Math","min","maxTokens","AI_DATASET_KIND","AI_DATASET_INSTRUCTIONS","text","recordsMin","recordsMax","valueMax","map","rule","join","cacheBreakpoint","aiDatasetPrompt","input","brief","trim","name","why","fields","length","field","shownIn","recordPages","generateAiDataset","row","step","settings","model","instructions","messages","role","content","tool","thinking","effort","signal","provider","check","answer","planned","merchantWords","aiDatasetDraftContent","dataset","options","records","values","record","forEach","column","value","type","split","part","filter","Boolean","replace","pageAddressFrom"],"mappings":"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"}
|
|
@@ -747,8 +747,53 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
|
|
|
747
747
|
* creation before the rules read the plan, when exactly one creation matches
|
|
748
748
|
* it with case, spaces and punctuation ignored. Two that match, or none, are
|
|
749
749
|
* left for the rules to ask about.
|
|
750
|
+
*
|
|
751
|
+
* So is a creation named without its prefix — `Contact form` in a section's
|
|
752
|
+
* uses for the "Contact form" the plan creates — where the name is no record
|
|
753
|
+
* the site has (AGL-3660). A re-asked plan told to place its form "by id"
|
|
754
|
+
* can answer with the form's bare name, which is no reference at all.
|
|
755
|
+
*/
|
|
756
|
+
export declare function aiSettlePlanRefs(plan: AiBuildPlan, inventory?: AiSiteInventory | null): AiBuildPlan;
|
|
757
|
+
/**
|
|
758
|
+
* What customers say, which no dataset of the site's holds: a review, a
|
|
759
|
+
* testimonial, a rating, a quote (AGL-3616). A job never writes them for the
|
|
760
|
+
* people who did not, so a dataset of them is one nothing can seed.
|
|
761
|
+
*/
|
|
762
|
+
export declare const AI_DATASET_SAID_NAME: RegExp;
|
|
763
|
+
/**
|
|
764
|
+
* A plan that creates more datasets than this job may make is settled in
|
|
765
|
+
* code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
|
|
766
|
+
* dental practice's plan four times over for a fourth dataset, and a food
|
|
767
|
+
* bank's re-ask for one broke a layout rule its first answer had kept.
|
|
768
|
+
*
|
|
769
|
+
* A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
|
|
770
|
+
* field) is let go first, whatever the cap: both of those plans made one of
|
|
771
|
+
* testimonials, which nothing may seed.
|
|
772
|
+
*
|
|
773
|
+
* The datasets kept are the most useful: the ones the most sections list,
|
|
774
|
+
* a record template counting double, plan order breaking ties. Each one let
|
|
775
|
+
* go leaves the plan whole — its `new:` reference comes off every section,
|
|
776
|
+
* a record template of it becomes a page of its own, and a section that
|
|
777
|
+
* listed it shows fewer items than rule 8 binds, so its items are written
|
|
778
|
+
* out on the page. Where the job may create no dataset at all, rule 7 still
|
|
779
|
+
* asks, as it does for any creation.
|
|
780
|
+
*/
|
|
781
|
+
export declare function aiSettlePlanDatasets(plan: AiBuildPlan, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
782
|
+
/**
|
|
783
|
+
* A form's dataset, settled in code before the rules read the plan
|
|
784
|
+
* (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
|
|
785
|
+
*
|
|
786
|
+
* - A `writesTo` that names nothing the form can write to comes off: a
|
|
787
|
+
* `new:` of no dataset this plan creates, an id the inventory does not
|
|
788
|
+
* hold, or a dataset this job may not create. The form is then the
|
|
789
|
+
* form it always was, its submissions in the Inbox. A dataset the
|
|
790
|
+
* inventory holds, named by its name rather than its id, is given its id.
|
|
791
|
+
* - A dataset this plan creates for a form holds the people who send it:
|
|
792
|
+
* no page lists it and none is its record template, so its `new:` comes
|
|
793
|
+
* off every section and record that named it, and a section that listed
|
|
794
|
+
* it shows fewer items than rule 8 binds.
|
|
750
795
|
*/
|
|
751
|
-
export declare function
|
|
796
|
+
export declare function aiSettlePlanFormDatasets(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
752
797
|
/**
|
|
753
798
|
* A page that names no layout, on a site where only one layout can frame it,
|
|
754
799
|
* is put in that layout before the rules read the plan (2026-10-07). The
|
|
@@ -760,6 +805,37 @@ export declare function aiSettlePlanRefs(plan: AiBuildPlan): AiBuildPlan;
|
|
|
760
805
|
* so the rule still asks.
|
|
761
806
|
*/
|
|
762
807
|
export declare function aiSettlePlanLayouts(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
808
|
+
/** The contact form a plan is given when a section collects answers and the plan makes no form. */
|
|
809
|
+
export declare const AI_PLAN_SETTLED_FORM: {
|
|
810
|
+
readonly name: "Contact form";
|
|
811
|
+
readonly why: "Visitors need a way to reach the owner, and the site has no saved form for it.";
|
|
812
|
+
readonly fields: readonly ["name", "email", "message"];
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* A section that collects answers and places no saved form is given one
|
|
816
|
+
* before the rules read the plan (AGL-3660), rather than failing the build.
|
|
817
|
+
* A production guided start for a ceramic artist's portfolio (2026-10-09,
|
|
818
|
+
* five pages, 94 credits refunded) planned a section that asks for
|
|
819
|
+
* commissions on its work page without placing the contact form the plan
|
|
820
|
+
* made for its Contact page; the answer and its re-ask both did, and rule 3
|
|
821
|
+
* failed the whole site for one section.
|
|
822
|
+
*
|
|
823
|
+
* The section places, in order of preference:
|
|
824
|
+
* 1. the form whose name best matches it, among those the plan creates and
|
|
825
|
+
* the site has, preferring one the plan already places — so a guided
|
|
826
|
+
* start, which makes one form, places that same form again rather than a
|
|
827
|
+
* second one;
|
|
828
|
+
* 2. where there is no form at all and this job may create one, a form the
|
|
829
|
+
* plan now creates: named for the `new:` reference the section already
|
|
830
|
+
* holds when it names a form it never declared, the contact form
|
|
831
|
+
* otherwise.
|
|
832
|
+
*
|
|
833
|
+
* Nothing is taken out of the plan: the section keeps its name, its other
|
|
834
|
+
* uses and its items. Where the site has no form and the job may create none,
|
|
835
|
+
* rule 3 asks for nothing (AGL-3596), and neither does this. A section whose
|
|
836
|
+
* uses are already full is left for the rule to ask about.
|
|
837
|
+
*/
|
|
838
|
+
export declare function aiSettlePlanForms(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
|
|
763
839
|
/**
|
|
764
840
|
* Rule 2 (plan): every screen declares a layout, no section is a copy of a
|
|
765
841
|
* layout region, and no section places a layout, which frames a whole screen
|
|
@@ -23,7 +23,7 @@ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
|
|
|
23
23
|
import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
|
|
24
24
|
import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
|
|
25
25
|
import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
|
|
26
|
-
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
26
|
+
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, AI_PLAN_DATASET_ITEM_OP, aiPlanCreateFor, aiPlanFormDatasetNames, aiPlanItemFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
27
27
|
import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
|
|
28
28
|
import { aiFreeSiteSectionsWithin, aiSiteHomeMinSections, aiSitePlanIsHome } from "../model/ai-site-job.js";
|
|
29
29
|
import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
|
|
@@ -279,12 +279,15 @@ function listNames(ids, tree) {
|
|
|
279
279
|
const index = indexShapes(tree);
|
|
280
280
|
/**
|
|
281
281
|
* A compiled picture card (AGL-3660): a Stack that opens with its picture,
|
|
282
|
-
* in a cell of a grid — or that cell, holding only the card.
|
|
282
|
+
* in a cell of a grid — or that cell, holding only the card. A storefront's
|
|
283
|
+
* icon column (AGL-3676), a Stack that opens with its library icon, is the
|
|
284
|
+
* compiler's the same way: no model writes an Icon on a page.
|
|
283
285
|
*/ const card = (id)=>{
|
|
284
286
|
var _node_nodes, _tree_nodes_first;
|
|
285
287
|
const node = id === undefined ? undefined : tree.nodes[id];
|
|
286
288
|
const first = node == null ? void 0 : (_node_nodes = node.nodes) == null ? void 0 : _node_nodes[0];
|
|
287
|
-
|
|
289
|
+
const opens = first === undefined ? undefined : (_tree_nodes_first = tree.nodes[first]) == null ? void 0 : _tree_nodes_first.componentId;
|
|
290
|
+
return (node == null ? void 0 : node.componentId) === 'muiStack' && (opens === 'image' || opens === 'icon');
|
|
288
291
|
};
|
|
289
292
|
const pictureCard = (id)=>{
|
|
290
293
|
var _node_props, _node_nodes, _index_visits_find, _tree_nodes_parentId;
|
|
@@ -3286,11 +3289,18 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3286
3289
|
* creation before the rules read the plan, when exactly one creation matches
|
|
3287
3290
|
* it with case, spaces and punctuation ignored. Two that match, or none, are
|
|
3288
3291
|
* left for the rules to ask about.
|
|
3289
|
-
|
|
3292
|
+
*
|
|
3293
|
+
* So is a creation named without its prefix — `Contact form` in a section's
|
|
3294
|
+
* uses for the "Contact form" the plan creates — where the name is no record
|
|
3295
|
+
* the site has (AGL-3660). A re-asked plan told to place its form "by id"
|
|
3296
|
+
* can answer with the form's bare name, which is no reference at all.
|
|
3297
|
+
*/ export function aiSettlePlanRefs(plan, inventory = null) {
|
|
3290
3298
|
if (!plan.create.length) return plan;
|
|
3299
|
+
const kinds = inventoryKinds(inventory);
|
|
3291
3300
|
const resolve = (ref)=>{
|
|
3292
|
-
if (!
|
|
3293
|
-
const key = creationKey(ref.slice('new:'.length));
|
|
3301
|
+
if (!ref || kinds.has(ref) || aiPlanCreateFor(plan, ref)) return ref;
|
|
3302
|
+
const key = creationKey(isAiPlanNewRef(ref) ? ref.slice('new:'.length) : ref);
|
|
3303
|
+
if (!key) return ref;
|
|
3294
3304
|
const matches = plan.create.filter((entry)=>creationKey(entry.name) === key);
|
|
3295
3305
|
return matches.length === 1 ? `new:${matches[0].name}` : ref;
|
|
3296
3306
|
};
|
|
@@ -3314,6 +3324,135 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3314
3324
|
screens
|
|
3315
3325
|
}) : plan;
|
|
3316
3326
|
}
|
|
3327
|
+
/**
|
|
3328
|
+
* What customers say, which no dataset of the site's holds: a review, a
|
|
3329
|
+
* testimonial, a rating, a quote (AGL-3616). A job never writes them for the
|
|
3330
|
+
* people who did not, so a dataset of them is one nothing can seed.
|
|
3331
|
+
*/ export const AI_DATASET_SAID_NAME = /\b(reviews?|testimonials?|ratings?|quotes?|stars?|endorsements?)\b/i;
|
|
3332
|
+
/**
|
|
3333
|
+
* A plan that creates more datasets than this job may make is settled in
|
|
3334
|
+
* code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
|
|
3335
|
+
* dental practice's plan four times over for a fourth dataset, and a food
|
|
3336
|
+
* bank's re-ask for one broke a layout rule its first answer had kept.
|
|
3337
|
+
*
|
|
3338
|
+
* A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
|
|
3339
|
+
* field) is let go first, whatever the cap: both of those plans made one of
|
|
3340
|
+
* testimonials, which nothing may seed.
|
|
3341
|
+
*
|
|
3342
|
+
* The datasets kept are the most useful: the ones the most sections list,
|
|
3343
|
+
* a record template counting double, plan order breaking ties. Each one let
|
|
3344
|
+
* go leaves the plan whole — its `new:` reference comes off every section,
|
|
3345
|
+
* a record template of it becomes a page of its own, and a section that
|
|
3346
|
+
* listed it shows fewer items than rule 8 binds, so its items are written
|
|
3347
|
+
* out on the page. Where the job may create no dataset at all, rule 7 still
|
|
3348
|
+
* asks, as it does for any creation.
|
|
3349
|
+
*/ export function aiSettlePlanDatasets(plan, capabilities = null) {
|
|
3350
|
+
var _ref, _entry_left;
|
|
3351
|
+
const entry = capabilities == null ? void 0 : capabilities.create.dataset;
|
|
3352
|
+
if (!(entry == null ? void 0 : entry.allowed)) return plan;
|
|
3353
|
+
const cap = Math.min((_ref = capabilities == null ? void 0 : capabilities.datasetsMax) != null ? _ref : Infinity, (_entry_left = entry.left) != null ? _entry_left : Infinity);
|
|
3354
|
+
const datasets = plan.create.filter((one)=>one.kind === 'dataset');
|
|
3355
|
+
// A dataset a form writes to holds what visitors send (AGL-3616): its
|
|
3356
|
+
// fields may well be a rating or a quote request, and nothing seeds it.
|
|
3357
|
+
const forForms = aiPlanFormDatasetNames(plan);
|
|
3358
|
+
const said = (one)=>!forForms.has(one.name.trim().toLowerCase()) && (AI_DATASET_SAID_NAME.test(one.name) || one.fields.some((field)=>AI_DATASET_SAID_NAME.test(field)));
|
|
3359
|
+
if (!datasets.some(said) && (!Number.isFinite(cap) || datasets.length <= cap)) return plan;
|
|
3360
|
+
const named = (ref, name)=>!!ref && isAiPlanNewRef(ref) && ref.slice('new:'.length).trim().toLowerCase() === name.trim().toLowerCase();
|
|
3361
|
+
// A form's dataset counts as much as a record template: the form the
|
|
3362
|
+
// person asked for is built around it.
|
|
3363
|
+
const score = (name)=>(forForms.has(name.trim().toLowerCase()) ? 2 : 0) + plan.screens.reduce((total, screen)=>{
|
|
3364
|
+
var _screen_record;
|
|
3365
|
+
return total + (named((_screen_record = screen.record) == null ? void 0 : _screen_record.dataset, name) ? 2 : 0) + screen.sections.filter((section)=>section.uses.some((ref)=>named(ref, name))).length;
|
|
3366
|
+
}, 0);
|
|
3367
|
+
const ranked = datasets.filter((one)=>!said(one)).map((one, index)=>({
|
|
3368
|
+
name: one.name,
|
|
3369
|
+
index,
|
|
3370
|
+
score: score(one.name)
|
|
3371
|
+
})).sort((a, b)=>b.score - a.score || a.index - b.index);
|
|
3372
|
+
const kept = new Set(ranked.slice(0, Math.max(0, cap)).map((one)=>one.name.trim().toLowerCase()));
|
|
3373
|
+
const dropped = new Set(datasets.map((one)=>one.name.trim().toLowerCase()).filter((name)=>!kept.has(name)));
|
|
3374
|
+
const isDropped = (ref)=>[
|
|
3375
|
+
...dropped
|
|
3376
|
+
].some((name)=>named(ref, name));
|
|
3377
|
+
return _extends({}, plan, {
|
|
3378
|
+
create: plan.create.filter((one)=>one.kind !== 'dataset' || !dropped.has(one.name.trim().toLowerCase()))// A form whose dataset was let go sends its submissions to the Inbox only.
|
|
3379
|
+
.map((one)=>one.kind === 'form' && isDropped(one.writesTo) ? withoutWritesTo(one) : one),
|
|
3380
|
+
screens: plan.screens.map((screen)=>_extends({}, screen, {
|
|
3381
|
+
record: screen.record && isDropped(screen.record.dataset) ? null : screen.record,
|
|
3382
|
+
sections: screen.sections.map((section)=>section.uses.some(isDropped) ? _extends({}, section, {
|
|
3383
|
+
uses: section.uses.filter((ref)=>!isDropped(ref)),
|
|
3384
|
+
items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
|
|
3385
|
+
}) : section)
|
|
3386
|
+
}))
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
/** A creation with no `writesTo`, as a plan that never said one reads. */ function withoutWritesTo(creation) {
|
|
3390
|
+
const rest = _extends({}, creation);
|
|
3391
|
+
delete rest.writesTo;
|
|
3392
|
+
return rest;
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* A form's dataset, settled in code before the rules read the plan
|
|
3396
|
+
* (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
|
|
3397
|
+
*
|
|
3398
|
+
* - A `writesTo` that names nothing the form can write to comes off: a
|
|
3399
|
+
* `new:` of no dataset this plan creates, an id the inventory does not
|
|
3400
|
+
* hold, or a dataset this job may not create. The form is then the
|
|
3401
|
+
* form it always was, its submissions in the Inbox. A dataset the
|
|
3402
|
+
* inventory holds, named by its name rather than its id, is given its id.
|
|
3403
|
+
* - A dataset this plan creates for a form holds the people who send it:
|
|
3404
|
+
* no page lists it and none is its record template, so its `new:` comes
|
|
3405
|
+
* off every section and record that named it, and a section that listed
|
|
3406
|
+
* it shows fewer items than rule 8 binds.
|
|
3407
|
+
*/ export function aiSettlePlanFormDatasets(plan, inventory, capabilities = null) {
|
|
3408
|
+
var _ref;
|
|
3409
|
+
if (!plan.create.some((one)=>one.kind === 'form' && one.writesTo)) return plan;
|
|
3410
|
+
const held = (_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : [];
|
|
3411
|
+
const creatable = !capabilities || capabilities.create.dataset.allowed;
|
|
3412
|
+
let changed = false;
|
|
3413
|
+
const create = plan.create.map((one)=>{
|
|
3414
|
+
if (one.kind !== 'form' || !one.writesTo) return one;
|
|
3415
|
+
const ref = one.writesTo.trim();
|
|
3416
|
+
if (isAiPlanNewRef(ref)) {
|
|
3417
|
+
var _aiPlanCreateFor, _aiPlanItemFor;
|
|
3418
|
+
if (creatable && ((_aiPlanCreateFor = aiPlanCreateFor(plan, ref)) == null ? void 0 : _aiPlanCreateFor.kind) === 'dataset') return one;
|
|
3419
|
+
// A build's dataset item, which the data plugin's operation makes.
|
|
3420
|
+
if (((_aiPlanItemFor = aiPlanItemFor(plan, ref)) == null ? void 0 : _aiPlanItemFor.op) === AI_PLAN_DATASET_ITEM_OP) return one;
|
|
3421
|
+
} else {
|
|
3422
|
+
if (held.some((row)=>row.id === ref)) return one;
|
|
3423
|
+
const byName = held.find((row)=>aiNamesMatch(row.name, ref));
|
|
3424
|
+
if (byName) {
|
|
3425
|
+
changed = true;
|
|
3426
|
+
return _extends({}, one, {
|
|
3427
|
+
writesTo: byName.id
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
changed = true;
|
|
3432
|
+
return withoutWritesTo(one);
|
|
3433
|
+
});
|
|
3434
|
+
const settled = _extends({}, plan, {
|
|
3435
|
+
create
|
|
3436
|
+
});
|
|
3437
|
+
const forForms = aiPlanFormDatasetNames(settled);
|
|
3438
|
+
const listsOne = (ref)=>isAiPlanNewRef(ref) && forForms.has(ref.slice('new:'.length).trim().toLowerCase());
|
|
3439
|
+
const screens = plan.screens.map((screen)=>{
|
|
3440
|
+
const record = screen.record && listsOne(screen.record.dataset) ? null : screen.record;
|
|
3441
|
+
const sections = screen.sections.map((section)=>section.uses.some(listsOne) ? _extends({}, section, {
|
|
3442
|
+
uses: section.uses.filter((ref)=>!listsOne(ref)),
|
|
3443
|
+
items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
|
|
3444
|
+
}) : section);
|
|
3445
|
+
if (record === screen.record && sections.every((section, index)=>section === screen.sections[index])) return screen;
|
|
3446
|
+
changed = true;
|
|
3447
|
+
return _extends({}, screen, {
|
|
3448
|
+
record,
|
|
3449
|
+
sections
|
|
3450
|
+
});
|
|
3451
|
+
});
|
|
3452
|
+
return changed ? _extends({}, settled, {
|
|
3453
|
+
screens
|
|
3454
|
+
}) : plan;
|
|
3455
|
+
}
|
|
3317
3456
|
/**
|
|
3318
3457
|
* A page that names no layout, on a site where only one layout can frame it,
|
|
3319
3458
|
* is put in that layout before the rules read the plan (2026-10-07). The
|
|
@@ -3366,6 +3505,140 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3366
3505
|
}))
|
|
3367
3506
|
});
|
|
3368
3507
|
}
|
|
3508
|
+
/** Every saved form a plan could place, the plan's own creations first. */ function planFormChoices(plan, inventory) {
|
|
3509
|
+
var _ref;
|
|
3510
|
+
return [
|
|
3511
|
+
...plan.create.filter((entry)=>entry.kind === 'form').map((entry)=>({
|
|
3512
|
+
ref: `new:${entry.name}`,
|
|
3513
|
+
name: entry.name,
|
|
3514
|
+
created: true
|
|
3515
|
+
})),
|
|
3516
|
+
...((_ref = inventory == null ? void 0 : inventory.forms) != null ? _ref : []).map((form)=>({
|
|
3517
|
+
ref: form.id,
|
|
3518
|
+
name: form.name,
|
|
3519
|
+
created: false
|
|
3520
|
+
}))
|
|
3521
|
+
];
|
|
3522
|
+
}
|
|
3523
|
+
/** The form a plan's own words would name for a section's reference, so placements count alike. */ function formChoiceKey(ref, plan) {
|
|
3524
|
+
const created = isAiPlanNewRef(ref) ? aiPlanCreateFor(plan, ref) : undefined;
|
|
3525
|
+
return created ? `new:${created.name}` : ref;
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* The form a section that collects answers places: the one whose name shares
|
|
3529
|
+
* the most words with the section's ("Commission inquiry" → "Commission
|
|
3530
|
+
* inquiry form"), then the one the plan already places most, then the plan's
|
|
3531
|
+
* own creation over a form the site has. `null` when there is none.
|
|
3532
|
+
*/ function bestPlanForm(section, choices, placed) {
|
|
3533
|
+
var _ranked_;
|
|
3534
|
+
const words = new Set(nameTokens(section.name).filter((word)=>word !== 'form'));
|
|
3535
|
+
const score = (choice)=>nameTokens(choice.name).filter((word)=>word !== 'form' && words.has(word)).length;
|
|
3536
|
+
const ranked = [
|
|
3537
|
+
...choices
|
|
3538
|
+
].sort((a, b)=>{
|
|
3539
|
+
var _placed_get, _placed_get1;
|
|
3540
|
+
return score(b) - score(a) || ((_placed_get = placed.get(b.ref)) != null ? _placed_get : 0) - ((_placed_get1 = placed.get(a.ref)) != null ? _placed_get1 : 0) || Number(b.created) - Number(a.created);
|
|
3541
|
+
});
|
|
3542
|
+
return (_ranked_ = ranked[0]) != null ? _ranked_ : null;
|
|
3543
|
+
}
|
|
3544
|
+
/** The contact form a plan is given when a section collects answers and the plan makes no form. */ export const AI_PLAN_SETTLED_FORM = {
|
|
3545
|
+
name: 'Contact form',
|
|
3546
|
+
why: 'Visitors need a way to reach the owner, and the site has no saved form for it.',
|
|
3547
|
+
fields: [
|
|
3548
|
+
'name',
|
|
3549
|
+
'email',
|
|
3550
|
+
'message'
|
|
3551
|
+
]
|
|
3552
|
+
};
|
|
3553
|
+
/**
|
|
3554
|
+
* A section that collects answers and places no saved form is given one
|
|
3555
|
+
* before the rules read the plan (AGL-3660), rather than failing the build.
|
|
3556
|
+
* A production guided start for a ceramic artist's portfolio (2026-10-09,
|
|
3557
|
+
* five pages, 94 credits refunded) planned a section that asks for
|
|
3558
|
+
* commissions on its work page without placing the contact form the plan
|
|
3559
|
+
* made for its Contact page; the answer and its re-ask both did, and rule 3
|
|
3560
|
+
* failed the whole site for one section.
|
|
3561
|
+
*
|
|
3562
|
+
* The section places, in order of preference:
|
|
3563
|
+
* 1. the form whose name best matches it, among those the plan creates and
|
|
3564
|
+
* the site has, preferring one the plan already places — so a guided
|
|
3565
|
+
* start, which makes one form, places that same form again rather than a
|
|
3566
|
+
* second one;
|
|
3567
|
+
* 2. where there is no form at all and this job may create one, a form the
|
|
3568
|
+
* plan now creates: named for the `new:` reference the section already
|
|
3569
|
+
* holds when it names a form it never declared, the contact form
|
|
3570
|
+
* otherwise.
|
|
3571
|
+
*
|
|
3572
|
+
* Nothing is taken out of the plan: the section keeps its name, its other
|
|
3573
|
+
* uses and its items. Where the site has no form and the job may create none,
|
|
3574
|
+
* rule 3 asks for nothing (AGL-3596), and neither does this. A section whose
|
|
3575
|
+
* uses are already full is left for the rule to ask about.
|
|
3576
|
+
*/ export function aiSettlePlanForms(plan, inventory, capabilities = null) {
|
|
3577
|
+
if (!aiPlanCanPlaceForm(inventory, capabilities)) return plan;
|
|
3578
|
+
const kinds = inventoryKinds(inventory);
|
|
3579
|
+
const unbound = (current)=>sectionPaths(current).filter(({ section })=>FORM_SECTION_NAME.test(section.name) && section.uses.length < AI_BUILD_PLAN_LIMITS.uses && !section.uses.some((ref)=>refKind(ref, current, kinds) === 'form'));
|
|
3580
|
+
const first = unbound(plan);
|
|
3581
|
+
if (!first.length) return plan;
|
|
3582
|
+
let next = plan;
|
|
3583
|
+
if (!planFormChoices(next, inventory).length) {
|
|
3584
|
+
if (capabilities && !capabilities.create.form.allowed) return plan;
|
|
3585
|
+
// A form the section already names and the plan never declared is the form it meant.
|
|
3586
|
+
const named = first.flatMap(({ section })=>section.uses).find((ref)=>isAiPlanNewRef(ref) && !aiPlanCreateFor(next, ref) && !aiPlanItemFor(next, ref));
|
|
3587
|
+
const taken = new Set(next.create.map((entry)=>entry.name.toLowerCase()));
|
|
3588
|
+
const name = named ? named.slice('new:'.length).trim() : taken.has(AI_PLAN_SETTLED_FORM.name.toLowerCase()) ? 'Get in touch form' : AI_PLAN_SETTLED_FORM.name;
|
|
3589
|
+
next = _extends({}, next, {
|
|
3590
|
+
create: [
|
|
3591
|
+
...next.create,
|
|
3592
|
+
{
|
|
3593
|
+
kind: 'form',
|
|
3594
|
+
name,
|
|
3595
|
+
why: AI_PLAN_SETTLED_FORM.why,
|
|
3596
|
+
duplicateOf: null,
|
|
3597
|
+
fields: [
|
|
3598
|
+
...AI_PLAN_SETTLED_FORM.fields
|
|
3599
|
+
]
|
|
3600
|
+
}
|
|
3601
|
+
]
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3604
|
+
const choices = planFormChoices(next, inventory);
|
|
3605
|
+
const placed = new Map();
|
|
3606
|
+
for (const { section } of sectionPaths(next)){
|
|
3607
|
+
for (const ref of section.uses){
|
|
3608
|
+
var _placed_get;
|
|
3609
|
+
if (refKind(ref, next, kinds) !== 'form') continue;
|
|
3610
|
+
const key = formChoiceKey(ref, next);
|
|
3611
|
+
placed.set(key, ((_placed_get = placed.get(key)) != null ? _placed_get : 0) + 1);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
const settle = new Map();
|
|
3615
|
+
for (const { section, path } of unbound(next)){
|
|
3616
|
+
const form = bestPlanForm(section, choices, placed);
|
|
3617
|
+
if (form) settle.set(path, form.ref);
|
|
3618
|
+
}
|
|
3619
|
+
if (!settle.size) return next;
|
|
3620
|
+
return _extends({}, next, {
|
|
3621
|
+
screens: next.screens.map((screen, screenIndex)=>_extends({}, screen, {
|
|
3622
|
+
sections: screen.sections.map((section, sectionIndex)=>{
|
|
3623
|
+
const ref = settle.get(`screens[${screenIndex}].sections[${sectionIndex}]`);
|
|
3624
|
+
if (!ref) return section;
|
|
3625
|
+
// The undeclared reference the form was named for now names it; any other stays.
|
|
3626
|
+
const uses = section.uses.some((use)=>formChoiceKey(use, next) === ref) ? section.uses : [
|
|
3627
|
+
...section.uses,
|
|
3628
|
+
ref
|
|
3629
|
+
];
|
|
3630
|
+
return _extends({}, section, {
|
|
3631
|
+
uses
|
|
3632
|
+
});
|
|
3633
|
+
})
|
|
3634
|
+
}))
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
/** The forms a section that collects answers can place, as the re-ask names them. */ function planFormChoicesText(plan, inventory) {
|
|
3638
|
+
const choices = planFormChoices(plan, inventory);
|
|
3639
|
+
if (!choices.length) return 'Plan one in create as a form, and put new:<its name> in the section\'s uses.';
|
|
3640
|
+
return `Put one of these in the section's uses: ${choices.map((choice)=>choice.created ? choice.ref : `${choice.ref} (${choice.name})`).join(', ')}.`;
|
|
3641
|
+
}
|
|
3369
3642
|
/**
|
|
3370
3643
|
* Rule 2 (plan): every screen declares a layout, no section is a copy of a
|
|
3371
3644
|
* layout region, and no section places a layout, which frames a whole screen
|
|
@@ -3426,6 +3699,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
3426
3699
|
rule: 3,
|
|
3427
3700
|
code: 'plan-form-not-placed',
|
|
3428
3701
|
message: 'A section collects answers without a form. Reuse a form from the Forms page, or plan one there, and place it by id.',
|
|
3702
|
+
detail: planFormChoicesText(plan, inventory),
|
|
3429
3703
|
paths: unbound.map((entry)=>entry.path)
|
|
3430
3704
|
}
|
|
3431
3705
|
] : [];
|