@aglyn/plugins-ai 1.0.0-beta.151 → 1.0.0-beta.153
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/jobs/ai-job-component-step.d.ts +27 -1
- package/src/lib/jobs/ai-job-component-step.js +68 -3
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +19 -0
- package/src/lib/jobs/ai-job-drafts.js +22 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +22 -1
- package/src/lib/jobs/ai-job-form-step.js +69 -1
- package/src/lib/jobs/ai-job-form-step.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-sections.d.ts +19 -6
- package/src/lib/jobs/ai-job-page-sections.js +20 -7
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.d.ts +22 -1
- package/src/lib/jobs/ai-job-template-step.js +59 -3
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +18 -1
- package/src/lib/jobs/fixtures/ai-page-briefs.js +79 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +21 -0
- package/src/lib/runtime/ai-doctrine-validators.js +50 -0
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +2 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-repeated-items.d.ts +13 -2
- package/src/lib/runtime/ai-repeated-items.js +41 -17
- package/src/lib/runtime/ai-repeated-items.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.153",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-errors": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.153",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.153",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.153",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.153",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.153",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.153",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.153",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.153",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.153",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.153",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.153",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.153",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.153",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.153",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.153",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.153",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource';
|
|
18
|
-
import type { AiJob, AiJobPlan } from '../model/ai-jobs.types';
|
|
18
|
+
import type { AiJob, AiJobPlan, AiJobReview } from '../model/ai-jobs.types';
|
|
19
19
|
import type { AiSystemBlock } from '../runtime/ai-runtime';
|
|
20
20
|
import { readSiteInventory } from '../runtime/site-inventory';
|
|
21
21
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
@@ -61,6 +61,32 @@ export declare const AI_JOB_COMPONENT_DEFAULT_NAME = "Component";
|
|
|
61
61
|
export declare const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[];
|
|
62
62
|
/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */
|
|
63
63
|
export declare function aiJobComponentPrompt(job: Pick<AiJob, 'brief'>, plan: AiJobPlan | null, name: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* The props the confirmed plan gives the component, against the component a
|
|
66
|
+
* COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or
|
|
67
|
+
* when the plan promised no prop to keep.
|
|
68
|
+
*
|
|
69
|
+
* The third kind to need this, after the layout (`aiCopiedLayoutReview`) and
|
|
70
|
+
* the page (`aiCopiedPageReview`), and found the same way: a duplicate branch
|
|
71
|
+
* generates nothing, so it is the one place a plan's promise can go unmet
|
|
72
|
+
* with no model to answer for it.
|
|
73
|
+
*
|
|
74
|
+
* MEASURED 2026-09-21. A plan read "the existing card has no link target; the
|
|
75
|
+
* brief needs one", and `practice-area-link-card` came out BYTE-IDENTICAL to
|
|
76
|
+
* `practice-area-card` — the same seven nodes under the same ids, the same
|
|
77
|
+
* three props, no link anywhere. Its job reported the copy as built. So did
|
|
78
|
+
* `practice-area-link-card-v2` the following pass. A component job can
|
|
79
|
+
* therefore charge a member for a duplicate of something they already had.
|
|
80
|
+
*
|
|
81
|
+
* A copy that cannot be read back settles nothing, and a promise this step
|
|
82
|
+
* cannot settle is reviewed by a person rather than reported as kept.
|
|
83
|
+
*/
|
|
84
|
+
export declare function aiCopiedComponentReview(firestore: FirebaseFirestore.Firestore, input: {
|
|
85
|
+
hostId: string;
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
plan: AiJobPlan | null;
|
|
89
|
+
}): Promise<AiJobReview | null>;
|
|
64
90
|
/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */
|
|
65
91
|
export declare const aiComponentJobAdmission: AiJobAdmission;
|
|
66
92
|
export interface AiJobComponentStepDeps {
|
|
@@ -22,9 +22,9 @@ import { readSiteInventory } from "../runtime/site-inventory.js";
|
|
|
22
22
|
import { AI_COMPONENT_TOOL_NAME, aiComponentPropKindWords, aiComponentTool } from "../tools/ai-component-tool.js";
|
|
23
23
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
24
24
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
25
|
-
import { aiComponentCheck } from "./ai-job-component-checks.js";
|
|
25
|
+
import { aiComponentCheck, aiPlannedComponentProps } from "./ai-job-component-checks.js";
|
|
26
26
|
import { aiJobDraftId } from "./ai-job-draft-ids.js";
|
|
27
|
-
import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, writeAiDraft } from "./ai-job-drafts.js";
|
|
27
|
+
import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, readAiDraftComponentProps, writeAiDraft } from "./ai-job-drafts.js";
|
|
28
28
|
import { aiBracketedFactsNote, aiConfirmedPlan, aiDoctrineReview, aiGenerationSpent, aiJobBriefLine, aiLimitReview, aiPlanCreation, aiPlanReferenceLines, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
29
29
|
import { registerAiJobStep } from "./ai-jobs.js";
|
|
30
30
|
/**
|
|
@@ -88,6 +88,53 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
88
88
|
...aiPlanReferenceLines(plan)
|
|
89
89
|
].join('\n');
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The props the confirmed plan gives the component, against the component a
|
|
93
|
+
* COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or
|
|
94
|
+
* when the plan promised no prop to keep.
|
|
95
|
+
*
|
|
96
|
+
* The third kind to need this, after the layout (`aiCopiedLayoutReview`) and
|
|
97
|
+
* the page (`aiCopiedPageReview`), and found the same way: a duplicate branch
|
|
98
|
+
* generates nothing, so it is the one place a plan's promise can go unmet
|
|
99
|
+
* with no model to answer for it.
|
|
100
|
+
*
|
|
101
|
+
* MEASURED 2026-09-21. A plan read "the existing card has no link target; the
|
|
102
|
+
* brief needs one", and `practice-area-link-card` came out BYTE-IDENTICAL to
|
|
103
|
+
* `practice-area-card` — the same seven nodes under the same ids, the same
|
|
104
|
+
* three props, no link anywhere. Its job reported the copy as built. So did
|
|
105
|
+
* `practice-area-link-card-v2` the following pass. A component job can
|
|
106
|
+
* therefore charge a member for a duplicate of something they already had.
|
|
107
|
+
*
|
|
108
|
+
* A copy that cannot be read back settles nothing, and a promise this step
|
|
109
|
+
* cannot settle is reviewed by a person rather than reported as kept.
|
|
110
|
+
*/ export async function aiCopiedComponentReview(firestore, input) {
|
|
111
|
+
const promised = aiPlannedComponentProps(input.plan);
|
|
112
|
+
if (!promised.length) return null;
|
|
113
|
+
const declared = await readAiDraftComponentProps(firestore, {
|
|
114
|
+
hostId: input.hostId,
|
|
115
|
+
id: input.id
|
|
116
|
+
});
|
|
117
|
+
if (declared === null) {
|
|
118
|
+
return {
|
|
119
|
+
reason: 'doctrine',
|
|
120
|
+
message: `"${input.name}" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,
|
|
121
|
+
findings: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const missing = promised.filter((name)=>!declared.includes(name));
|
|
125
|
+
if (!missing.length) return null;
|
|
126
|
+
const one = missing.length === 1;
|
|
127
|
+
return aiDoctrineReview({
|
|
128
|
+
message: `"${input.name}" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${one ? 'a property' : 'properties'} the copy does not have.`,
|
|
129
|
+
violations: [
|
|
130
|
+
{
|
|
131
|
+
rule: null,
|
|
132
|
+
code: 'plan-props-missing',
|
|
133
|
+
message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${declared.length ? declared.join(', ') : 'none of them'}. Add ${one ? 'it' : 'each of them'}, and use ${one ? 'it' : 'them'} in the component, or the copy is the component it was copied from.`
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
91
138
|
/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */ export const aiComponentJobAdmission = (context)=>aiDraftAdmissionRefusal(context.firestore, {
|
|
92
139
|
orgId: context.orgId,
|
|
93
140
|
hostId: context.hostId,
|
|
@@ -148,11 +195,29 @@ export function createAiJobComponentStep(deps = {}) {
|
|
|
148
195
|
sourceId: creation.duplicateOf,
|
|
149
196
|
name,
|
|
150
197
|
uid: job.createdBy,
|
|
151
|
-
org: org
|
|
198
|
+
org: org,
|
|
199
|
+
// The same claim the page step makes (AGL-3024): a copy core mints is
|
|
200
|
+
// written under core's own id, never `draftId`, so a step that runs
|
|
201
|
+
// again — a pass resumed, a run cut off after the copy — would mint a
|
|
202
|
+
// second. The job's recorded id makes core replay the one it made.
|
|
203
|
+
attemptKey: draftId
|
|
152
204
|
});
|
|
153
205
|
if (copy.ok) {
|
|
154
206
|
var _copy_versionId;
|
|
155
207
|
const hostSubdomain = await aiSiteSubdomain(firestore, hostId);
|
|
208
|
+
// The copy is reported with what the plan promised of it read back
|
|
209
|
+
// (AGL-3024): this branch generates nothing, so without this a plan
|
|
210
|
+
// reading "duplicate it and add a link" reports Done on a duplicate
|
|
211
|
+
// that added nothing.
|
|
212
|
+
const review = await aiCopiedComponentReview(firestore, {
|
|
213
|
+
hostId,
|
|
214
|
+
id: copy.id,
|
|
215
|
+
name: copy.name,
|
|
216
|
+
plan
|
|
217
|
+
});
|
|
218
|
+
if (review) return aiUnspentOutcome(model, {
|
|
219
|
+
review
|
|
220
|
+
});
|
|
156
221
|
return aiUnspentOutcome(model, {
|
|
157
222
|
outputs: [
|
|
158
223
|
output({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.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 { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n context: { definesComponent: true },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiComponentJobAdmission","context","firestore","orgId","hostId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","id","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","ok","status","review","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","extend","onProps","declared","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-component-step.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 { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { ReusableComponentProp } from '@aglyn/aglyn/foundation/definitions/platform.types'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport type { AiJob, AiJobOutput, AiJobPlan, AiJobReview } from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runValidatedGeneration } from '../runtime/ai-doctrine'\nimport type { AiDoctrineTree } from '../runtime/ai-doctrine-validators'\nimport type { AiLoadEstimate } from '../runtime/ai-palette'\nimport type { AiSystemBlock } from '../runtime/ai-runtime'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n AI_COMPONENT_TOOL_NAME,\n aiComponentPropKindWords,\n aiComponentTool,\n} from '../tools/ai-component-tool'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiComponentCheck, aiPlannedComponentProps } from './ai-job-component-checks'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport {\n aiDraftAdmissionRefusal,\n aiDraftAllowanceRefusal,\n aiSiteSubdomain,\n readAiDraft,\n readAiDraftComponentProps,\n writeAiDraft,\n type AiDraftRecord,\n} from './ai-job-drafts'\nimport {\n aiBracketedFactsNote,\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiJobBriefLine,\n aiLimitReview,\n aiPlanCreation,\n aiPlanReferenceLines,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport type { AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The component step (AGL-2908): the generation step of a `component` job,\n * run once a member confirmed the job's plan.\n *\n * It builds ONE reusable component from the brief — its tree, and the typed\n * properties each page that places it fills in — through\n * `runValidatedGeneration('component', …)`, so every building rule holds on\n * the tree before it is kept. The tree binds each property with\n * `{{prop.<name>}}`; the palette validator keeps those tokens as written\n * (`definesComponent`), and the step's own check, through `extend`, holds\n * each binding to what the Properties dialog and the Attributes panel allow\n * (`ai-job-component-checks.ts`).\n *\n * The component lands as a new draft (`ai-job-drafts.ts`) that no page\n * places, and gets its first version when a member opens it, as every\n * component created outside the besigner does. A plan that starts from a copy\n * of a component the site has gets that copy, through the platform's\n * duplicate module, and nothing generated.\n *\n * A generation step runs on the job beat, never at an inline door: it\n * registers the least time its lookup rounds, its answer and its re-ask need\n * (AGL-3035), runs without extended thinking, and asks for no more of the\n * ceiling the layout and template steps keep than fits that time on the model\n * it runs. The step's spec measures the request and the answer against it.\n */\n\n/** The longest answer a component may run to; the tree is held to the component budget either way. */\nexport const AI_JOB_COMPONENT_MAX_TOKENS = AI_ROUTING_TABLE['job.component'].maxTokens\n\n/**\n * The component step's time (AGL-3035, AGL-3036): its two inventory-lookup\n * rounds, its answer and its re-ask at the routing table's ceiling on the tier\n * `job.component` is served from, fitted to what a beat can give a step.\n */\nexport const AI_JOB_COMPONENT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.component'],\n maxTokens: AI_JOB_COMPONENT_MAX_TOKENS,\n})\n\n/** The least time one component step needs before it starts. */\nexport const AI_JOB_COMPONENT_STEP_MINIMUM_MS = AI_JOB_COMPONENT_STEP_BUDGET.minimumMs\n\n/** A component's name when the plan names none. */\nexport const AI_JOB_COMPONENT_DEFAULT_NAME = 'Component'\n\n/** The component step's own instructions, cached after the doctrine and before the catalog. */\nexport const AI_JOB_COMPONENT_INSTRUCTIONS: readonly AiSystemBlock[] = [\n {\n text: [\n `You build one reusable component for a website: a block the site places on many pages, each placement filling in its own values. Answer with ${AI_COMPONENT_TOOL_NAME}: the component’s tree as one flat node map, and the properties it declares.`,\n 'The root is the document wrapper holding the component. A component is placed inside a page section, which opens with its own h2: it carries no main landmark, its headings are h3 or below, and its colors, spacing and type come from the theme.',\n 'Every value that should differ from one placement to the next is a property, bound in the tree with its token {{prop.<name>}}. Declare every property the tree binds, and bind every property you declare.',\n `Property kinds, as type (the name a page reads): ${aiComponentPropKindWords()}.`,\n 'A property binds only to a field that holds its kind of value:',\n '- text, richText and number: copy, such as a Typography’s text, an Image’s alt or a Button’s label, as the whole value or inside a sentence.',\n '- image: an Image’s src, as the whole value.',\n '- href: a screenId or an href, as the whole value.',\n '- icon: an Icon element’s iconId, as the whole value.',\n '- boolean: a switch setting such as a Button’s fullWidth, or hideIf on the part it hides, as the whole value.',\n '- choice: a setting with fixed options such as a Button’s variant, as the whole value, with every answer’s value one that setting lists.',\n 'An optional part, such as a photo, a badge or a second button, gets a boolean property labeled \"Hide <the part>\" whose default is \"false\", and the part’s own element carries \"hideIf\": \"{{prop.<name>}}\". Never hide the whole component.',\n 'A default is what the component shows until a page sets it: copy in the site’s voice from the brief, with a fact the brief does not give in square brackets; for href a screen id from the site inventory, or \"\"; for image \"\", for an upload; for icon \"\", for the site owner to pick; for choice one of its answers’ values.',\n ].join('\\n'),\n },\n]\n\n/** The component job as the generation's user turn: the name, the brief, the confirmed plan. */\nexport function aiJobComponentPrompt(\n job: Pick<AiJob, 'brief'>,\n plan: AiJobPlan | null,\n name: string,\n): string {\n return [`Component name: ${name}`, aiJobBriefLine(job), ...aiPlanReferenceLines(plan)].join('\\n')\n}\n\n/**\n * The props the confirmed plan gives the component, against the component a\n * COPY actually produced (AGL-3024); `null` when the copy keeps the plan, or\n * when the plan promised no prop to keep.\n *\n * The third kind to need this, after the layout (`aiCopiedLayoutReview`) and\n * the page (`aiCopiedPageReview`), and found the same way: a duplicate branch\n * generates nothing, so it is the one place a plan's promise can go unmet\n * with no model to answer for it.\n *\n * MEASURED 2026-09-21. A plan read \"the existing card has no link target; the\n * brief needs one\", and `practice-area-link-card` came out BYTE-IDENTICAL to\n * `practice-area-card` — the same seven nodes under the same ids, the same\n * three props, no link anywhere. Its job reported the copy as built. So did\n * `practice-area-link-card-v2` the following pass. A component job can\n * therefore charge a member for a duplicate of something they already had.\n *\n * A copy that cannot be read back settles nothing, and a promise this step\n * cannot settle is reviewed by a person rather than reported as kept.\n */\nexport async function aiCopiedComponentReview(\n firestore: FirebaseFirestore.Firestore,\n input: { hostId: string; id: string; name: string; plan: AiJobPlan | null },\n): Promise<AiJobReview | null> {\n const promised = aiPlannedComponentProps(input.plan)\n if (!promised.length) return null\n const declared = await readAiDraftComponentProps(firestore, { hostId: input.hostId, id: input.id })\n if (declared === null) {\n return {\n reason: 'doctrine',\n message: `\"${input.name}\" was copied from the component the plan names, and this job could not read the copy back to check it has the properties the plan gives it. Open the component and check it before you place it.`,\n findings: [],\n }\n }\n const missing = promised.filter((name) => !declared.includes(name))\n if (!missing.length) return null\n const one = missing.length === 1\n return aiDoctrineReview({\n message: `\"${input.name}\" is a copy of the component the plan names, and a copy is all it is: the plan gives it ${\n one ? 'a property' : 'properties'\n } the copy does not have.`,\n violations: [\n {\n rule: null,\n code: 'plan-props-missing',\n message: `The confirmed plan says this component has ${missing.join(', ')}, and the copy has ${\n declared.length ? declared.join(', ') : 'none of them'\n }. Add ${one ? 'it' : 'each of them'}, and use ${\n one ? 'it' : 'them'\n } in the component, or the copy is the component it was copied from.`,\n },\n ],\n })\n}\n\n/** A component job is admitted for a site of the job's own org whose plan includes reusable components. */\nexport const aiComponentJobAdmission: AiJobAdmission = (context) =>\n aiDraftAdmissionRefusal(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n kind: 'component',\n org: context.org,\n })\n\nexport interface AiJobComponentStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** The platform's duplicate module, for a plan that starts from a copy. */\n duplicate?: typeof duplicateResource\n}\n\nexport function createAiJobComponentStep(deps: AiJobComponentStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const duplicate = deps.duplicate ?? duplicateResource\n return async ({ job, now, signal, firestore, modelFor }) => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a component job names no site, and its admission refuses one')\n const output = (draft: AiDraftRecord, load?: AiLoadEstimate | null, note?: string | null): AiJobOutput => ({\n resource: 'reusableComponent',\n id: draft.id,\n versionId: draft.versionId,\n hostId,\n hostSubdomain: draft.hostSubdomain,\n label: draft.name,\n ...(load ? { load } : {}),\n ...(note ? { note } : {}),\n })\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.component') ?? aiModelForStep('job.component')\n\n // A run cut off after its draft was written reports that draft, found by the id the job recorded.\n const draftId = aiJobDraftId(job, 'component')\n const written = await readAiDraft(firestore, { kind: 'component', hostId, id: draftId })\n if (written) return aiUnspentOutcome(model, { outputs: [output(written)] })\n\n const [inventory, orgSnapshot] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n firestore.collection('orgs').doc(job.orgId).get(),\n ])\n const org = (orgSnapshot.data() ?? null) as Partial<AglynOrgBilling> | null\n const plan = aiConfirmedPlan(job)\n const creation = aiPlanCreation(plan, 'component')\n const name = creation?.name || AI_JOB_COMPONENT_DEFAULT_NAME\n\n // The plan starts from a copy of a component the site has (rule 15): the\n // copy is the draft, and nothing is generated. A source gone since the\n // plan was made is built from the brief instead.\n if (\n creation?.duplicateOf &&\n inventory.components.some((component) => component.id === creation.duplicateOf)\n ) {\n const copy = await duplicate('component', {\n orgId: job.orgId,\n hostId,\n sourceId: creation.duplicateOf,\n name,\n uid: job.createdBy,\n org: org as Record<string, unknown> | null,\n // The same claim the page step makes (AGL-3024): a copy core mints is\n // written under core's own id, never `draftId`, so a step that runs\n // again — a pass resumed, a run cut off after the copy — would mint a\n // second. The job's recorded id makes core replay the one it made.\n attemptKey: draftId,\n })\n if (copy.ok) {\n const hostSubdomain = await aiSiteSubdomain(firestore, hostId)\n // The copy is reported with what the plan promised of it read back\n // (AGL-3024): this branch generates nothing, so without this a plan\n // reading \"duplicate it and add a link\" reports Done on a duplicate\n // that added nothing.\n const review = await aiCopiedComponentReview(firestore, {\n hostId,\n id: copy.id,\n name: copy.name,\n plan,\n })\n if (review) return aiUnspentOutcome(model, { review })\n return aiUnspentOutcome(model, {\n outputs: [\n output({ id: copy.id, versionId: copy.versionId ?? null, name: copy.name, hostSubdomain }),\n ],\n })\n }\n if (copy.ok === false && copy.status === 403) {\n return aiUnspentOutcome(model, { review: aiLimitReview(copy.error) })\n }\n }\n\n const allowance = await aiDraftAllowanceRefusal(firestore, { kind: 'component', hostId, org })\n if (allowance) return aiUnspentOutcome(model, { review: aiLimitReview(allowance) })\n\n let props: ReusableComponentProp[] = []\n const result = await runValidatedGeneration('component', {\n step: 'job.component',\n model,\n instructions: AI_JOB_COMPONENT_INSTRUCTIONS,\n inventory,\n messages: [{ role: 'user', content: aiJobComponentPrompt(job, plan, name) }],\n tool: aiComponentTool(),\n maxTokens: AI_JOB_COMPONENT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.component'].thinking\n ? { thinking: AI_ROUTING_TABLE['job.component'].thinking }\n : {}),\n ...(AI_ROUTING_TABLE['job.component'].effort\n ? { effort: AI_ROUTING_TABLE['job.component'].effort }\n : {}),\n context: { definesComponent: true },\n extend: aiComponentCheck({\n inventory,\n plan,\n onProps: (declared) => {\n props = declared\n },\n }),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n\n const draft = await writeAiDraft(firestore, {\n kind: 'component',\n hostId,\n id: draftId,\n uid: job.createdBy,\n org,\n name,\n nodes: result.value.nodes,\n rootId: result.value.rootId,\n props,\n now,\n })\n if (draft.ok === false) {\n if (draft.status === 404) throw new Error(`site ${hostId} vanished while its component was generated`)\n return { ...spent, review: aiLimitReview(draft.error) }\n }\n // The facts the brief did not give, in the tree or the defaults a page shows (AGL-3056).\n const note = aiBracketedFactsNote({\n tree: { rootId: result.value.rootId, nodes: result.value.nodes as unknown as AiDoctrineTree['nodes'] },\n inventory,\n copy: props.map((prop) => (typeof prop.defaultValue === 'string' ? prop.defaultValue : '')),\n })\n return { ...spent, outputs: [output(draft, result.value.load, note)] }\n }\n}\n\nexport const runAiJobComponentStep = createAiJobComponentStep()\n\n/** Registers the component step and the check a component job passes before it is created or resumed. */\nexport function registerAiComponentJob(): void {\n registerAiJobStep('component', runAiJobComponentStep, { minimumMs: AI_JOB_COMPONENT_STEP_MINIMUM_MS })\n registerAiJobAdmission('component', aiComponentJobAdmission)\n}\n"],"names":["duplicateResource","AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","readSiteInventory","AI_COMPONENT_TOOL_NAME","aiComponentPropKindWords","aiComponentTool","registerAiJobAdmission","aiJobStepBudget","aiComponentCheck","aiPlannedComponentProps","aiJobDraftId","aiDraftAdmissionRefusal","aiDraftAllowanceRefusal","aiSiteSubdomain","readAiDraft","readAiDraftComponentProps","writeAiDraft","aiBracketedFactsNote","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiJobBriefLine","aiLimitReview","aiPlanCreation","aiPlanReferenceLines","aiUnspentOutcome","registerAiJobStep","AI_JOB_COMPONENT_MAX_TOKENS","maxTokens","AI_JOB_COMPONENT_STEP_BUDGET","tier","AI_JOB_COMPONENT_STEP_MINIMUM_MS","minimumMs","AI_JOB_COMPONENT_DEFAULT_NAME","AI_JOB_COMPONENT_INSTRUCTIONS","text","join","aiJobComponentPrompt","job","plan","name","aiCopiedComponentReview","firestore","input","promised","length","declared","hostId","id","reason","message","findings","missing","filter","includes","one","violations","rule","code","aiComponentJobAdmission","context","orgId","kind","org","createAiJobComponentStep","deps","readInventory","duplicate","now","signal","modelFor","orgSnapshot","Error","output","draft","load","note","resource","versionId","hostSubdomain","label","model","draftId","written","outputs","inventory","Promise","all","collection","doc","get","data","creation","duplicateOf","components","some","component","copy","sourceId","uid","createdBy","attemptKey","ok","review","status","error","allowance","props","result","step","instructions","messages","role","content","tool","thinking","effort","definesComponent","extend","onProps","spent","refused","nodes","value","rootId","tree","map","prop","defaultValue","runAiJobComponentStep","registerAiComponentJob"],"mappings":"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"}
|
|
@@ -192,6 +192,25 @@ export declare function readAiDraft(firestore: Firestore, input: {
|
|
|
192
192
|
hostId: string;
|
|
193
193
|
id: string;
|
|
194
194
|
}): Promise<AiDraftRecord | null>;
|
|
195
|
+
/**
|
|
196
|
+
* The tree a template draft holds now, decoded; `null` when there is none.
|
|
197
|
+
*
|
|
198
|
+
* A template is not a versioned draft — its nodes live on the document
|
|
199
|
+
* itself, which is why `readAiDraftNodes` refuses its kind.
|
|
200
|
+
*/
|
|
201
|
+
export declare function readAiTemplateDraftNodes(firestore: Firestore, input: {
|
|
202
|
+
hostId: string;
|
|
203
|
+
id: string;
|
|
204
|
+
}): Promise<NodesMap | null>;
|
|
205
|
+
/**
|
|
206
|
+
* The property names a component draft declares now; `null` when there is no
|
|
207
|
+
* such draft. Read from the draft document rather than its version, because a
|
|
208
|
+
* component's props live beside its tree and a copy carries its source's.
|
|
209
|
+
*/
|
|
210
|
+
export declare function readAiDraftComponentProps(firestore: Firestore, input: {
|
|
211
|
+
hostId: string;
|
|
212
|
+
id: string;
|
|
213
|
+
}): Promise<string[] | null>;
|
|
195
214
|
/** The tree a versioned draft's first version holds now, decoded; `null` when there is none. */
|
|
196
215
|
export declare function readAiDraftNodes(firestore: Firestore, input: {
|
|
197
216
|
kind: AiVersionedDraftKind;
|
|
@@ -303,6 +303,28 @@ function recordOf(id, draft, host) {
|
|
|
303
303
|
if (!draft.exists) return null;
|
|
304
304
|
return recordOf(input.id, draft, await firestore.collection('hosts').doc(input.hostId).get());
|
|
305
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* The tree a template draft holds now, decoded; `null` when there is none.
|
|
308
|
+
*
|
|
309
|
+
* A template is not a versioned draft — its nodes live on the document
|
|
310
|
+
* itself, which is why `readAiDraftNodes` refuses its kind.
|
|
311
|
+
*/ export async function readAiTemplateDraftNodes(firestore, input) {
|
|
312
|
+
var _decodeStoredNodes;
|
|
313
|
+
const draft = await draftCollection(firestore, input.hostId, 'template').doc(input.id).get();
|
|
314
|
+
if (!draft.exists) return null;
|
|
315
|
+
return (_decodeStoredNodes = decodeStoredNodes(draft.get('nodes'))) != null ? _decodeStoredNodes : null;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The property names a component draft declares now; `null` when there is no
|
|
319
|
+
* such draft. Read from the draft document rather than its version, because a
|
|
320
|
+
* component's props live beside its tree and a copy carries its source's.
|
|
321
|
+
*/ export async function readAiDraftComponentProps(firestore, input) {
|
|
322
|
+
const draft = await draftCollection(firestore, input.hostId, 'component').doc(input.id).get();
|
|
323
|
+
if (!draft.exists) return null;
|
|
324
|
+
const props = draft.get('props');
|
|
325
|
+
if (!Array.isArray(props)) return [];
|
|
326
|
+
return props.map((prop)=>prop && typeof prop === 'object' ? prop['name'] : null).filter((name)=>typeof name === 'string' && name.length > 0);
|
|
327
|
+
}
|
|
306
328
|
/** The tree a versioned draft's first version holds now, decoded; `null` when there is none. */ export async function readAiDraftNodes(firestore, input) {
|
|
307
329
|
const draftRef = draftCollection(firestore, input.hostId, input.kind).doc(input.id);
|
|
308
330
|
const draft = await draftRef.get();
|