@aglyn/plugins-ai 1.0.0-beta.244 → 1.0.0-beta.246
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/activity/ai-activity-actions.js +3 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-links.js +7 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +3 -1
- package/src/lib/components/ai-product-zones.js.map +1 -1
- package/src/lib/components/ai-products-hub-card.component.js +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.js +17 -9
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +6 -4
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js +1 -1
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +162 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-build-step.js +85 -16
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +123 -0
- package/src/lib/jobs/ai-job-duplicate-step.js +269 -0
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -0
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +157 -0
- package/src/lib/jobs/ai-job-navigation-step.js +439 -0
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +21 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +1 -1
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-plan-step.js +16 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-products-step.js +2 -2
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +272 -0
- package/src/lib/jobs/ai-job-settings-step.js +695 -0
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -0
- package/src/lib/jobs/ai-job-site-content.d.ts +20 -6
- package/src/lib/jobs/ai-job-site-content.js +28 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-datasets.js +10 -3
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +3 -1
- package/src/lib/jobs/ai-job-site-step.js +70 -10
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +62 -4
- package/src/lib/jobs/ai-site-publish.js +203 -56
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.d.ts +7 -4
- package/src/lib/layout-language/ai-layout-compiler.js +26 -25
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.d.ts +41 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js +330 -0
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +18 -1
- package/src/lib/layout-language/ai-layout-frame.js +83 -13
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +35 -0
- package/src/lib/layout-language/ai-layout-listings.js +34 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-tree.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-tree.js +17 -0
- package/src/lib/layout-language/ai-layout-tree.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +23 -0
- package/src/lib/model/ai-build-job.js +102 -13
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +25 -0
- package/src/lib/model/ai-build-plan.js +45 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +52 -0
- package/src/lib/model/ai-credit-estimate.js +51 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +4 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +10 -2
- package/src/lib/model/ai-jobs.types.js +4 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-layout-menus.d.ts +104 -0
- package/src/lib/model/ai-layout-menus.js +518 -0
- package/src/lib/model/ai-layout-menus.js.map +1 -0
- package/src/lib/model/ai-products.d.ts +31 -1
- package/src/lib/model/ai-products.js +34 -1
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +15 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +10 -0
- package/src/lib/model/ai-site-job.js +36 -9
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.js +1 -1
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +53 -0
- package/src/lib/runtime/ai-doctrine-validators.js +166 -25
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +7 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +1 -1
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +4 -2
- package/src/lib/runtime/ai-products-generation.js +12 -6
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +12 -0
- package/src/lib/runtime/site-inventory.js +29 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +37 -3
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
- package/src/lib/tools/ai-products-tool.d.ts +3 -1
- package/src/lib/tools/ai-products-tool.js +15 -5
- package/src/lib/tools/ai-products-tool.js.map +1 -1
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
18
|
+
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { decodeStoredNodes, encodeStoredNodes } from "@aglyn/aglyn/app-utils/stored-nodes";
|
|
20
|
+
import { getLockdownVerdict } from "@aglyn/tenant-data-admin/server/lockdown";
|
|
21
|
+
import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
22
|
+
import { AI_EDIT_GONE_COPY, AI_EDIT_NEEDS_VERSIONING_COPY, AI_EDIT_NO_NODES_COPY, AI_EDIT_VERSION_GONE_COPY } from "../model/ai-edit-job.js";
|
|
23
|
+
import { isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
24
|
+
import { AI_BUILD_MADE_INPUT } from "../model/ai-build-job.js";
|
|
25
|
+
import { aiBuildStoreLayoutOf } from "../model/ai-build-store-pages.js";
|
|
26
|
+
import { aiLayoutWithMenuChanges, aiMenuChangesEmpty, aiMenuChangesOf, aiMenusOf } from "../model/ai-layout-menus.js";
|
|
27
|
+
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
28
|
+
import { aiOriginJobId } from "./ai-job-draft-ids.js";
|
|
29
|
+
import { aiEditPlacement, aiEditVersionId, aiJobEditModel, readAiEditTarget } from "./ai-job-edit-step.js";
|
|
30
|
+
import { aiLimitReview, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
31
|
+
import { registerAiJobStep } from "./ai-jobs.js";
|
|
32
|
+
/**
|
|
33
|
+
* The `navigation` step (AGL-3728): a change to a site's header or footer
|
|
34
|
+
* menu — add, take out, rename, relink or reorder its items — as a
|
|
35
|
+
* first-class build operation, so Assist's one plan card can say "add About
|
|
36
|
+
* and Pricing to the menu, and take Blog out" and have it done.
|
|
37
|
+
*
|
|
38
|
+
* NO MODEL. The planner already wrote the change as the item's arguments, in
|
|
39
|
+
* words (`aiMenuChangesOf`); this applies it to the layout's own Besigner
|
|
40
|
+
* elements in code (`aiLayoutWithMenuChanges`), so it costs no credits and
|
|
41
|
+
* cannot invent a link: every destination is one of the site's pages, a page
|
|
42
|
+
* this build made, a path on the site or an https address.
|
|
43
|
+
*
|
|
44
|
+
* ── Where it lands ───────────────────────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* As the `edit` step's change to a layout does: a NEW version beside the
|
|
47
|
+
* layout's current one, which visitors keep seeing until a person publishes
|
|
48
|
+
* the new one. A layout is always served, so without version history there is
|
|
49
|
+
* nowhere for it to land and it is refused as a plan limit, charged nothing.
|
|
50
|
+
* The version is named by the job's id, so a pass asked again finds what it
|
|
51
|
+
* wrote.
|
|
52
|
+
*
|
|
53
|
+
* ── Who may ──────────────────────────────────────────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* The editor's own rule for a layout's versions (`canWriteHostContent` and
|
|
56
|
+
* `hostWritesFrozen` in the Firestore rules): a member with a write role on
|
|
57
|
+
* the site (admin, editor or author), while no lockdown freezes the site, its
|
|
58
|
+
* org or the platform. Asked at the door, again before the write, and the
|
|
59
|
+
* role once more inside the transaction that writes.
|
|
60
|
+
*
|
|
61
|
+
* ── Audit ────────────────────────────────────────────────────────────────
|
|
62
|
+
*
|
|
63
|
+
* The layout it wrote is the job's output, which the job machine records as
|
|
64
|
+
* an `ai.job.output` activity row on the site, as every other operation's is.
|
|
65
|
+
*/ export const AI_NAVIGATION_KIND = 'navigation';
|
|
66
|
+
/**
|
|
67
|
+
* ASSUMED: the least time the step needs — the site's pages, the layout and
|
|
68
|
+
* its version, the org, and the one transaction that writes. No model, so it
|
|
69
|
+
* fits an inline door.
|
|
70
|
+
*/ export const AI_JOB_NAVIGATION_STEP_MINIMUM_MS = 5000;
|
|
71
|
+
export const AI_NAVIGATION_NO_SITE_COPY = 'Open the site whose menu should change.';
|
|
72
|
+
export const AI_NAVIGATION_ROLE_COPY = 'Changing the site’s menus needs the admin, editor or author role on this site.';
|
|
73
|
+
export const AI_NAVIGATION_LOCKED_COPY = 'This site is not accepting changes right now.';
|
|
74
|
+
export const AI_NAVIGATION_NO_LAYOUT_COPY = 'This site has no layout with a header or footer to change. Make one in Layouts first.';
|
|
75
|
+
export const AI_NAVIGATION_NOTHING_COPY = 'The request named no menu change this could make.';
|
|
76
|
+
/** The name the new version carries in the layout's versions. */ export const AI_NAVIGATION_VERSION_NAME = 'Menu changes';
|
|
77
|
+
const text = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
78
|
+
/** A path on the site, as a menu item may link it. */ const SITE_PATH = /^\/(?:[A-Za-z0-9._~!$&'()*+,;=:@%-]+(?:\/[A-Za-z0-9._~!$&'()*+,;=:@%-]+)*)?\/?(?:[?#][^\s]*)?$/;
|
|
79
|
+
const SCREEN_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
80
|
+
function buildMadeOf(inputs) {
|
|
81
|
+
var _ref;
|
|
82
|
+
const raw = (_ref = inputs == null ? void 0 : inputs[AI_BUILD_MADE_INPUT]) != null ? _ref : {};
|
|
83
|
+
const rows = (value, name)=>(Array.isArray(value) ? value : []).flatMap((entry)=>{
|
|
84
|
+
const row = entry != null ? entry : {};
|
|
85
|
+
return text(row[name]) && SCREEN_ID.test(text(row['id'])) ? [
|
|
86
|
+
{
|
|
87
|
+
[name]: text(row[name]),
|
|
88
|
+
id: text(row['id'])
|
|
89
|
+
}
|
|
90
|
+
] : [];
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
pages: rows(raw.pages, 'title'),
|
|
94
|
+
layouts: rows(raw.layouts, 'name')
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** The site's pages: not deleted, not an email design. */ export async function readAiNavigationSitePages(firestore, hostId) {
|
|
98
|
+
const snapshot = await firestore.collection('hosts').doc(hostId).collection('screens').select('displayName', 'slug', 'kind', 'deletedAt', 'layoutId').get();
|
|
99
|
+
return snapshot.docs.flatMap((doc)=>{
|
|
100
|
+
var _doc_get, _doc_get1;
|
|
101
|
+
const kind = doc.get('kind');
|
|
102
|
+
if (doc.get('deletedAt') || kind && kind !== 'template') return [];
|
|
103
|
+
const layoutId = doc.get('layoutId');
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
id: doc.id,
|
|
107
|
+
slug: String((_doc_get = doc.get('slug')) != null ? _doc_get : ''),
|
|
108
|
+
title: String((_doc_get1 = doc.get('displayName')) != null ? _doc_get1 : ''),
|
|
109
|
+
template: kind === 'template',
|
|
110
|
+
layoutId: typeof layoutId === 'string' && layoutId ? layoutId : null
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* What a destination in an item's words links (AGL-3728): a page this build
|
|
117
|
+
* made (`new:<title>`), one of the site's pages by id (`page:<id>` or the bare
|
|
118
|
+
* id), a path on the site, or an https address; `null` for anything else.
|
|
119
|
+
*/ export function aiNavigationTarget(destination, context) {
|
|
120
|
+
const value = destination.trim();
|
|
121
|
+
const pageIds = new Set(context.pages.filter((page)=>!page.template).map((page)=>page.id));
|
|
122
|
+
if (isAiPlanNewRef(value)) {
|
|
123
|
+
const title = value.slice(value.indexOf(':') + 1).trim().toLowerCase();
|
|
124
|
+
const made = context.made.pages.find((page)=>page.title.trim().toLowerCase() === title);
|
|
125
|
+
// A page this build was to make and did not is no destination.
|
|
126
|
+
return made && pageIds.has(made.id) ? {
|
|
127
|
+
screenId: made.id
|
|
128
|
+
} : null;
|
|
129
|
+
}
|
|
130
|
+
const id = value.replace(/^page:/i, '');
|
|
131
|
+
if (pageIds.has(id)) return {
|
|
132
|
+
screenId: id
|
|
133
|
+
};
|
|
134
|
+
if (value.startsWith('/') && !value.startsWith('//') && SITE_PATH.test(value)) return {
|
|
135
|
+
href: value
|
|
136
|
+
};
|
|
137
|
+
if (/^https:\/\/[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/i.test(value)) return {
|
|
138
|
+
href: value
|
|
139
|
+
};
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
/** The layout an item changes: the one it names, else the one the site's pages render inside. */ export function aiNavigationLayoutOf(named, context) {
|
|
143
|
+
if (isAiPlanNewRef(named)) {
|
|
144
|
+
var _ref;
|
|
145
|
+
var _context_made_layouts_find;
|
|
146
|
+
const name = named.slice(named.indexOf(':') + 1).trim().toLowerCase();
|
|
147
|
+
return (_ref = (_context_made_layouts_find = context.made.layouts.find((layout)=>layout.name.trim().toLowerCase() === name)) == null ? void 0 : _context_made_layouts_find.id) != null ? _ref : null;
|
|
148
|
+
}
|
|
149
|
+
if (SCREEN_ID.test(named)) return named;
|
|
150
|
+
return aiBuildStoreLayoutOf(null, context.pages);
|
|
151
|
+
}
|
|
152
|
+
/** Why this member may not change this site's menus now, or `null`. */ export async function aiNavigationAccessRefusal(input, deps = {}) {
|
|
153
|
+
var _input_host_get, _deps_lockdown;
|
|
154
|
+
const roles = (_input_host_get = input.host.get('memberRoles')) != null ? _input_host_get : {};
|
|
155
|
+
if (!hostRoleCanWrite(roles[input.uid])) return AI_NAVIGATION_ROLE_COPY;
|
|
156
|
+
const locked = await ((_deps_lockdown = deps.lockdown) != null ? _deps_lockdown : getLockdownVerdict)({
|
|
157
|
+
staff: false,
|
|
158
|
+
uid: input.uid,
|
|
159
|
+
org: input.org,
|
|
160
|
+
host: input.host.data(),
|
|
161
|
+
intent: 'write'
|
|
162
|
+
});
|
|
163
|
+
return locked ? AI_NAVIGATION_LOCKED_COPY : null;
|
|
164
|
+
}
|
|
165
|
+
/** The new version's output note: what was done, what was not, and that it waits to be published. */ export function aiNavigationOutputNote(done, notDone) {
|
|
166
|
+
const did = done.length ? `${done.join(' ')} ` : '';
|
|
167
|
+
const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : '';
|
|
168
|
+
return `${did}Saved as a new version of the layout beside the one visitors see: open it to review it, and publish it when it is ready.${not}`.slice(0, 1000);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The change written, in one transaction that reads the layout as it is
|
|
172
|
+
* stored NOW: it still exists, its version is still there, the member still
|
|
173
|
+
* holds a write role, and the plan still keeps version history. The change is
|
|
174
|
+
* applied again to that version's nodes, so a save made since is kept.
|
|
175
|
+
*/ export async function writeAiNavigation(firestore, input) {
|
|
176
|
+
const hostRef = firestore.collection('hosts').doc(input.hostId);
|
|
177
|
+
const docRef = hostRef.collection('layouts').doc(input.layoutId);
|
|
178
|
+
const sourceRef = docRef.collection('versions').doc(input.sourceVersionId);
|
|
179
|
+
const newRef = docRef.collection('versions').doc(input.versionId);
|
|
180
|
+
return firestore.runTransaction(async (tx)=>{
|
|
181
|
+
var _host_get;
|
|
182
|
+
const [host, doc, source, already] = await Promise.all([
|
|
183
|
+
tx.get(hostRef),
|
|
184
|
+
tx.get(docRef),
|
|
185
|
+
tx.get(sourceRef),
|
|
186
|
+
tx.get(newRef)
|
|
187
|
+
]);
|
|
188
|
+
if (already.exists) return {
|
|
189
|
+
ok: true,
|
|
190
|
+
versionId: input.versionId,
|
|
191
|
+
done: [],
|
|
192
|
+
notDone: []
|
|
193
|
+
};
|
|
194
|
+
if (!doc.exists || doc.get('deletedAt') != null) return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: AI_EDIT_GONE_COPY
|
|
197
|
+
};
|
|
198
|
+
if (!source.exists) return {
|
|
199
|
+
ok: false,
|
|
200
|
+
error: AI_EDIT_VERSION_GONE_COPY
|
|
201
|
+
};
|
|
202
|
+
const roles = (_host_get = host.get('memberRoles')) != null ? _host_get : {};
|
|
203
|
+
if (!hostRoleCanWrite(roles[input.uid])) return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: AI_NAVIGATION_ROLE_COPY
|
|
206
|
+
};
|
|
207
|
+
const pointer = text(doc.get('versionId')) || null;
|
|
208
|
+
const placement = aiEditPlacement({
|
|
209
|
+
versioning: input.versioning,
|
|
210
|
+
served: true,
|
|
211
|
+
sourceIsPointer: pointer === input.sourceVersionId,
|
|
212
|
+
sourceScheduled: false
|
|
213
|
+
});
|
|
214
|
+
if (placement === 'refused') return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: AI_EDIT_NEEDS_VERSIONING_COPY
|
|
217
|
+
};
|
|
218
|
+
const nodes = decodeStoredNodes(source.get('nodes'));
|
|
219
|
+
if (!nodes) return {
|
|
220
|
+
ok: false,
|
|
221
|
+
error: AI_EDIT_NO_NODES_COPY
|
|
222
|
+
};
|
|
223
|
+
const applied = aiLayoutWithMenuChanges(nodes, input.menus, input.changes);
|
|
224
|
+
if (!applied.nodes) return {
|
|
225
|
+
ok: false,
|
|
226
|
+
error: applied.notDone.join(' ') || AI_NAVIGATION_NOTHING_COPY
|
|
227
|
+
};
|
|
228
|
+
const packed = encodeStoredNodes(applied.nodes);
|
|
229
|
+
if (!packed) return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: AI_EDIT_NO_NODES_COPY
|
|
232
|
+
};
|
|
233
|
+
// A copy of the version it started from — everything the editor stored —
|
|
234
|
+
// with the changed menus, as the versions route copies one.
|
|
235
|
+
const copy = _extends({}, source.data());
|
|
236
|
+
delete copy['aiEditJobId'];
|
|
237
|
+
tx.create(newRef, _extends({}, copy, {
|
|
238
|
+
nodes: Buffer.from(packed),
|
|
239
|
+
displayName: AI_NAVIGATION_VERSION_NAME,
|
|
240
|
+
createdAt: input.now,
|
|
241
|
+
updatedAt: input.now,
|
|
242
|
+
createdBy: input.uid,
|
|
243
|
+
aiJobId: input.aiJobId
|
|
244
|
+
}));
|
|
245
|
+
return {
|
|
246
|
+
ok: true,
|
|
247
|
+
versionId: input.versionId,
|
|
248
|
+
done: applied.done,
|
|
249
|
+
notDone: applied.notDone
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async function navigationPlanOf(firestore, job, readPages) {
|
|
254
|
+
var _job_inputs;
|
|
255
|
+
if (!job.hostId) return AI_NAVIGATION_NO_SITE_COPY;
|
|
256
|
+
const inputs = (_job_inputs = job.inputs) != null ? _job_inputs : {};
|
|
257
|
+
const pages = await readPages(firestore, job.hostId);
|
|
258
|
+
const made = buildMadeOf(inputs);
|
|
259
|
+
const layoutId = aiNavigationLayoutOf(text(inputs['layout']), {
|
|
260
|
+
pages,
|
|
261
|
+
made
|
|
262
|
+
});
|
|
263
|
+
if (!layoutId) return AI_NAVIGATION_NO_LAYOUT_COPY;
|
|
264
|
+
const read = aiMenuChangesOf(inputs, (destination)=>aiNavigationTarget(destination, {
|
|
265
|
+
pages,
|
|
266
|
+
made
|
|
267
|
+
}));
|
|
268
|
+
if (aiMenuChangesEmpty(read.changes)) return [
|
|
269
|
+
AI_NAVIGATION_NOTHING_COPY,
|
|
270
|
+
...read.problems
|
|
271
|
+
].join(' ');
|
|
272
|
+
return {
|
|
273
|
+
layoutId,
|
|
274
|
+
menus: aiMenusOf(inputs['menu']),
|
|
275
|
+
changes: read.changes,
|
|
276
|
+
problems: read.problems
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function createAiJobNavigationStep(deps = {}) {
|
|
280
|
+
var _deps_readTarget, _deps_readPages, _deps_write;
|
|
281
|
+
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
282
|
+
const readPages = (_deps_readPages = deps.readPages) != null ? _deps_readPages : readAiNavigationSitePages;
|
|
283
|
+
const write = (_deps_write = deps.write) != null ? _deps_write : writeAiNavigation;
|
|
284
|
+
return async ({ job, now, firestore, org, modelFor })=>{
|
|
285
|
+
var _ref, _data;
|
|
286
|
+
// No model is asked: the outcome records the edit step's at no cost.
|
|
287
|
+
const model = (_ref = modelFor == null ? void 0 : modelFor('job.edit')) != null ? _ref : aiJobEditModel();
|
|
288
|
+
const unspent = (failure)=>_extends({}, aiUnspentOutcome(model), {
|
|
289
|
+
failure
|
|
290
|
+
});
|
|
291
|
+
const hostId = job.hostId;
|
|
292
|
+
if (!hostId) return unspent(AI_NAVIGATION_NO_SITE_COPY);
|
|
293
|
+
const plan = await navigationPlanOf(firestore, job, readPages);
|
|
294
|
+
if (typeof plan === 'string') return unspent(plan);
|
|
295
|
+
const versionId = aiEditVersionId(job);
|
|
296
|
+
const read = await readTarget(firestore, {
|
|
297
|
+
orgId: job.orgId,
|
|
298
|
+
hostId,
|
|
299
|
+
inputs: {
|
|
300
|
+
target: plan.layoutId,
|
|
301
|
+
targetKind: 'layout',
|
|
302
|
+
versionId: null
|
|
303
|
+
},
|
|
304
|
+
jobId: versionId
|
|
305
|
+
});
|
|
306
|
+
if ('error' in read) return unspent(read.error);
|
|
307
|
+
const output = (done, notDone)=>({
|
|
308
|
+
resource: 'layout',
|
|
309
|
+
id: read.id,
|
|
310
|
+
versionId,
|
|
311
|
+
hostId,
|
|
312
|
+
hostSubdomain: read.hostSubdomain,
|
|
313
|
+
label: read.name,
|
|
314
|
+
note: aiNavigationOutputNote(done, notDone)
|
|
315
|
+
});
|
|
316
|
+
if (read.written) return aiUnspentOutcome(model, {
|
|
317
|
+
outputs: [
|
|
318
|
+
output([], [])
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
const orgData = org != null ? org : (_data = (await firestore.collection('orgs').doc(job.orgId).get()).data()) != null ? _data : null;
|
|
322
|
+
const host = await firestore.collection('hosts').doc(hostId).get();
|
|
323
|
+
const refused = await aiNavigationAccessRefusal({
|
|
324
|
+
host,
|
|
325
|
+
uid: job.createdBy,
|
|
326
|
+
org: orgData
|
|
327
|
+
}, {
|
|
328
|
+
lockdown: deps.lockdown
|
|
329
|
+
});
|
|
330
|
+
if (refused) return aiUnspentOutcome(model, {
|
|
331
|
+
review: aiLimitReview(refused)
|
|
332
|
+
});
|
|
333
|
+
const versioning = checkEntitlement(orgData, 'versioning');
|
|
334
|
+
const placement = aiEditPlacement({
|
|
335
|
+
versioning,
|
|
336
|
+
served: true,
|
|
337
|
+
sourceIsPointer: read.sourceVersionId === read.pointerVersionId,
|
|
338
|
+
sourceScheduled: read.scheduledVersionId === read.sourceVersionId
|
|
339
|
+
});
|
|
340
|
+
if (placement === 'refused') return aiUnspentOutcome(model, {
|
|
341
|
+
review: aiLimitReview(AI_EDIT_NEEDS_VERSIONING_COPY)
|
|
342
|
+
});
|
|
343
|
+
const written = await write(firestore, {
|
|
344
|
+
hostId,
|
|
345
|
+
layoutId: read.id,
|
|
346
|
+
sourceVersionId: read.sourceVersionId,
|
|
347
|
+
versionId,
|
|
348
|
+
menus: plan.menus,
|
|
349
|
+
changes: plan.changes,
|
|
350
|
+
uid: job.createdBy,
|
|
351
|
+
aiJobId: aiOriginJobId(job),
|
|
352
|
+
versioning,
|
|
353
|
+
now
|
|
354
|
+
});
|
|
355
|
+
if (written.ok === false) return unspent(written.error);
|
|
356
|
+
return aiUnspentOutcome(model, {
|
|
357
|
+
outputs: [
|
|
358
|
+
output(written.done, [
|
|
359
|
+
...written.notDone,
|
|
360
|
+
...plan.problems
|
|
361
|
+
])
|
|
362
|
+
]
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
export const runAiJobNavigationStep = createAiJobNavigationStep();
|
|
367
|
+
/**
|
|
368
|
+
* A navigation change is admitted on a site of the job's own org, for a
|
|
369
|
+
* member who may write its content now, where the site has a layout and the
|
|
370
|
+
* change has somewhere to land that no visitor sees on this plan. Refused in
|
|
371
|
+
* those words before anything exists.
|
|
372
|
+
*/ export function createAiNavigationJobAdmission(deps = {}) {
|
|
373
|
+
var _deps_ownerOf, _deps_readTarget, _deps_readPages;
|
|
374
|
+
const ownerOf = (_deps_ownerOf = deps.ownerOf) != null ? _deps_ownerOf : resolveOrgIdForHost;
|
|
375
|
+
const readTarget = (_deps_readTarget = deps.readTarget) != null ? _deps_readTarget : readAiEditTarget;
|
|
376
|
+
const readPages = (_deps_readPages = deps.readPages) != null ? _deps_readPages : readAiNavigationSitePages;
|
|
377
|
+
return async (context)=>{
|
|
378
|
+
var _context_uid;
|
|
379
|
+
if (!context.hostId) return {
|
|
380
|
+
status: 400,
|
|
381
|
+
error: AI_NAVIGATION_NO_SITE_COPY
|
|
382
|
+
};
|
|
383
|
+
const owner = await ownerOf(context.hostId);
|
|
384
|
+
if (!owner || owner !== context.orgId) return {
|
|
385
|
+
status: 404,
|
|
386
|
+
error: 'Unknown site'
|
|
387
|
+
};
|
|
388
|
+
const host = await context.firestore.collection('hosts').doc(context.hostId).get();
|
|
389
|
+
const refused = await aiNavigationAccessRefusal({
|
|
390
|
+
host,
|
|
391
|
+
uid: (_context_uid = context.uid) != null ? _context_uid : '',
|
|
392
|
+
org: context.org
|
|
393
|
+
}, {
|
|
394
|
+
lockdown: deps.lockdown
|
|
395
|
+
});
|
|
396
|
+
if (refused) return {
|
|
397
|
+
status: 403,
|
|
398
|
+
error: refused
|
|
399
|
+
};
|
|
400
|
+
const plan = await navigationPlanOf(context.firestore, {
|
|
401
|
+
hostId: context.hostId,
|
|
402
|
+
inputs: context.inputs
|
|
403
|
+
}, readPages);
|
|
404
|
+
if (typeof plan === 'string') return {
|
|
405
|
+
status: 400,
|
|
406
|
+
error: plan
|
|
407
|
+
};
|
|
408
|
+
const read = await readTarget(context.firestore, {
|
|
409
|
+
orgId: context.orgId,
|
|
410
|
+
hostId: context.hostId,
|
|
411
|
+
inputs: {
|
|
412
|
+
target: plan.layoutId,
|
|
413
|
+
targetKind: 'layout',
|
|
414
|
+
versionId: null
|
|
415
|
+
},
|
|
416
|
+
jobId: null
|
|
417
|
+
});
|
|
418
|
+
if ('error' in read) return read;
|
|
419
|
+
const placement = aiEditPlacement({
|
|
420
|
+
versioning: checkEntitlement(context.org, 'versioning'),
|
|
421
|
+
served: true,
|
|
422
|
+
sourceIsPointer: read.sourceVersionId === read.pointerVersionId,
|
|
423
|
+
sourceScheduled: read.scheduledVersionId === read.sourceVersionId
|
|
424
|
+
});
|
|
425
|
+
return placement === 'refused' ? {
|
|
426
|
+
status: 403,
|
|
427
|
+
error: AI_EDIT_NEEDS_VERSIONING_COPY
|
|
428
|
+
} : null;
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
export const aiNavigationJobAdmission = createAiNavigationJobAdmission();
|
|
432
|
+
/** Registers the navigation step and its door (AGL-3728). */ export function registerAiNavigationJob() {
|
|
433
|
+
registerAiJobStep(AI_NAVIGATION_KIND, runAiJobNavigationStep, {
|
|
434
|
+
minimumMs: AI_JOB_NAVIGATION_STEP_MINIMUM_MS
|
|
435
|
+
});
|
|
436
|
+
registerAiJobAdmission(AI_NAVIGATION_KIND, aiNavigationJobAdmission);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//# sourceMappingURL=ai-job-navigation-step.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-navigation-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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { decodeStoredNodes, encodeStoredNodes } from '@aglyn/aglyn/app-utils/stored-nodes'\nimport { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type { Firestore } from 'firebase-admin/firestore'\nimport { AI_EDIT_GONE_COPY, AI_EDIT_NEEDS_VERSIONING_COPY, AI_EDIT_NO_NODES_COPY, AI_EDIT_VERSION_GONE_COPY } from '../model/ai-edit-job'\nimport { isAiPlanNewRef } from '../model/ai-build-plan'\nimport { AI_BUILD_MADE_INPUT, type AiBuildMade } from '../model/ai-build-job'\nimport { aiBuildStoreLayoutOf } from '../model/ai-build-store-pages'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n aiLayoutWithMenuChanges,\n aiMenuChangesEmpty,\n aiMenuChangesOf,\n aiMenusOf,\n type AiMenuChanges,\n type AiMenuName,\n type AiMenuTarget,\n} from '../model/ai-layout-menus'\nimport { registerAiJobAdmission, type AiJobAdmission, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { aiEditPlacement, aiEditVersionId, aiJobEditModel, readAiEditTarget } from './ai-job-edit-step'\nimport { aiLimitReview, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `navigation` step (AGL-3728): a change to a site's header or footer\n * menu — add, take out, rename, relink or reorder its items — as a\n * first-class build operation, so Assist's one plan card can say \"add About\n * and Pricing to the menu, and take Blog out\" and have it done.\n *\n * NO MODEL. The planner already wrote the change as the item's arguments, in\n * words (`aiMenuChangesOf`); this applies it to the layout's own Besigner\n * elements in code (`aiLayoutWithMenuChanges`), so it costs no credits and\n * cannot invent a link: every destination is one of the site's pages, a page\n * this build made, a path on the site or an https address.\n *\n * ── Where it lands ───────────────────────────────────────────────────────\n *\n * As the `edit` step's change to a layout does: a NEW version beside the\n * layout's current one, which visitors keep seeing until a person publishes\n * the new one. A layout is always served, so without version history there is\n * nowhere for it to land and it is refused as a plan limit, charged nothing.\n * The version is named by the job's id, so a pass asked again finds what it\n * wrote.\n *\n * ── Who may ──────────────────────────────────────────────────────────────\n *\n * The editor's own rule for a layout's versions (`canWriteHostContent` and\n * `hostWritesFrozen` in the Firestore rules): a member with a write role on\n * the site (admin, editor or author), while no lockdown freezes the site, its\n * org or the platform. Asked at the door, again before the write, and the\n * role once more inside the transaction that writes.\n *\n * ── Audit ────────────────────────────────────────────────────────────────\n *\n * The layout it wrote is the job's output, which the job machine records as\n * an `ai.job.output` activity row on the site, as every other operation's is.\n */\n\nexport const AI_NAVIGATION_KIND = 'navigation' as const\n\n/**\n * ASSUMED: the least time the step needs — the site's pages, the layout and\n * its version, the org, and the one transaction that writes. No model, so it\n * fits an inline door.\n */\nexport const AI_JOB_NAVIGATION_STEP_MINIMUM_MS = 5_000\n\nexport const AI_NAVIGATION_NO_SITE_COPY = 'Open the site whose menu should change.'\nexport const AI_NAVIGATION_ROLE_COPY = 'Changing the site’s menus needs the admin, editor or author role on this site.'\nexport const AI_NAVIGATION_LOCKED_COPY = 'This site is not accepting changes right now.'\nexport const AI_NAVIGATION_NO_LAYOUT_COPY = 'This site has no layout with a header or footer to change. Make one in Layouts first.'\nexport const AI_NAVIGATION_NOTHING_COPY = 'The request named no menu change this could make.'\n\n/** The name the new version carries in the layout's versions. */\nexport const AI_NAVIGATION_VERSION_NAME = 'Menu changes'\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** A path on the site, as a menu item may link it. */\nconst SITE_PATH = /^\\/(?:[A-Za-z0-9._~!$&'()*+,;=:@%-]+(?:\\/[A-Za-z0-9._~!$&'()*+,;=:@%-]+)*)?\\/?(?:[?#][^\\s]*)?$/\nconst SCREEN_ID = /^[A-Za-z0-9_-]{1,128}$/\n\nfunction buildMadeOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiBuildMade {\n const raw = (inputs?.[AI_BUILD_MADE_INPUT] ?? {}) as Partial<Record<'pages' | 'layouts', unknown>>\n const rows = (value: unknown, name: 'title' | 'name') =>\n (Array.isArray(value) ? value : []).flatMap((entry) => {\n const row = (entry ?? {}) as Record<string, unknown>\n return text(row[name]) && SCREEN_ID.test(text(row['id'])) ? [{ [name]: text(row[name]), id: text(row['id']) }] : []\n })\n return { pages: rows(raw.pages, 'title') as AiBuildMade['pages'], layouts: rows(raw.layouts, 'name') as AiBuildMade['layouts'] }\n}\n\n/** The site's pages a menu may link, by id, with the layout each renders inside. */\nexport interface AiNavigationSitePage {\n id: string\n title: string\n slug: string\n template: boolean\n layoutId: string | null\n}\n\n/** The site's pages: not deleted, not an email design. */\nexport async function readAiNavigationSitePages(firestore: Firestore, hostId: string): Promise<AiNavigationSitePage[]> {\n const snapshot = await firestore.collection('hosts').doc(hostId).collection('screens').select('displayName', 'slug', 'kind', 'deletedAt', 'layoutId').get()\n return snapshot.docs.flatMap((doc) => {\n const kind = doc.get('kind')\n if (doc.get('deletedAt') || (kind && kind !== 'template')) return []\n const layoutId = doc.get('layoutId')\n return [\n {\n id: doc.id,\n slug: String(doc.get('slug') ?? ''),\n title: String(doc.get('displayName') ?? ''),\n template: kind === 'template',\n layoutId: typeof layoutId === 'string' && layoutId ? layoutId : null,\n },\n ]\n })\n}\n\n/**\n * What a destination in an item's words links (AGL-3728): a page this build\n * made (`new:<title>`), one of the site's pages by id (`page:<id>` or the bare\n * id), a path on the site, or an https address; `null` for anything else.\n */\nexport function aiNavigationTarget(\n destination: string,\n context: { pages: readonly Pick<AiNavigationSitePage, 'id' | 'template'>[]; made: AiBuildMade },\n): AiMenuTarget | null {\n const value = destination.trim()\n const pageIds = new Set(context.pages.filter((page) => !page.template).map((page) => page.id))\n if (isAiPlanNewRef(value as string | null)) {\n const title = value.slice(value.indexOf(':') + 1).trim().toLowerCase()\n const made = context.made.pages.find((page) => page.title.trim().toLowerCase() === title)\n // A page this build was to make and did not is no destination.\n return made && pageIds.has(made.id) ? { screenId: made.id } : null\n }\n const id = value.replace(/^page:/i, '')\n if (pageIds.has(id)) return { screenId: id }\n if (value.startsWith('/') && !value.startsWith('//') && SITE_PATH.test(value)) return { href: value }\n if (/^https:\\/\\/[^\\s/?#]+\\.[^\\s/?#]+(?:[/?#]\\S*)?$/i.test(value)) return { href: value }\n return null\n}\n\n/** The layout an item changes: the one it names, else the one the site's pages render inside. */\nexport function aiNavigationLayoutOf(\n named: string,\n context: { pages: readonly AiNavigationSitePage[]; made: AiBuildMade },\n): string | null {\n if (isAiPlanNewRef(named as string | null)) {\n const name = named.slice(named.indexOf(':') + 1).trim().toLowerCase()\n return context.made.layouts.find((layout) => layout.name.trim().toLowerCase() === name)?.id ?? null\n }\n if (SCREEN_ID.test(named)) return named\n return aiBuildStoreLayoutOf(null, context.pages)\n}\n\n/** Why this member may not change this site's menus now, or `null`. */\nexport async function aiNavigationAccessRefusal(\n input: { host: FirebaseFirestore.DocumentSnapshot; uid: string; org: object | null },\n deps: { lockdown?: typeof getLockdownVerdict } = {},\n): Promise<string | null> {\n const roles = (input.host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return AI_NAVIGATION_ROLE_COPY\n const locked = await (deps.lockdown ?? getLockdownVerdict)({\n staff: false,\n uid: input.uid,\n org: input.org as never,\n host: input.host.data() as never,\n intent: 'write',\n })\n return locked ? AI_NAVIGATION_LOCKED_COPY : null\n}\n\n/** The new version's output note: what was done, what was not, and that it waits to be published. */\nexport function aiNavigationOutputNote(done: readonly string[], notDone: readonly string[]): string {\n const did = done.length ? `${done.join(' ')} ` : ''\n const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : ''\n return `${did}Saved as a new version of the layout beside the one visitors see: open it to review it, and publish it when it is ready.${not}`.slice(0, 1_000)\n}\n\nexport type AiNavigationWrite =\n | { ok: true; versionId: string; done: string[]; notDone: string[] }\n | { ok: false; error: string }\n\n/**\n * The change written, in one transaction that reads the layout as it is\n * stored NOW: it still exists, its version is still there, the member still\n * holds a write role, and the plan still keeps version history. The change is\n * applied again to that version's nodes, so a save made since is kept.\n */\nexport async function writeAiNavigation(\n firestore: Firestore,\n input: {\n hostId: string\n layoutId: string\n sourceVersionId: string\n versionId: string\n menus: readonly AiMenuName[]\n changes: AiMenuChanges\n uid: string\n aiJobId: string\n versioning: boolean\n now: Date\n },\n): Promise<AiNavigationWrite> {\n const hostRef = firestore.collection('hosts').doc(input.hostId)\n const docRef = hostRef.collection('layouts').doc(input.layoutId)\n const sourceRef = docRef.collection('versions').doc(input.sourceVersionId)\n const newRef = docRef.collection('versions').doc(input.versionId)\n return firestore.runTransaction(async (tx): Promise<AiNavigationWrite> => {\n const [host, doc, source, already] = await Promise.all([tx.get(hostRef), tx.get(docRef), tx.get(sourceRef), tx.get(newRef)])\n if (already.exists) return { ok: true, versionId: input.versionId, done: [], notDone: [] }\n if (!doc.exists || doc.get('deletedAt') != null) return { ok: false, error: AI_EDIT_GONE_COPY }\n if (!source.exists) return { ok: false, error: AI_EDIT_VERSION_GONE_COPY }\n const roles = (host.get('memberRoles') ?? {}) as Record<string, unknown>\n if (!hostRoleCanWrite(roles[input.uid])) return { ok: false, error: AI_NAVIGATION_ROLE_COPY }\n const pointer = text(doc.get('versionId')) || null\n const placement = aiEditPlacement({\n versioning: input.versioning,\n served: true,\n sourceIsPointer: pointer === input.sourceVersionId,\n sourceScheduled: false,\n })\n if (placement === 'refused') return { ok: false, error: AI_EDIT_NEEDS_VERSIONING_COPY }\n const nodes = decodeStoredNodes<Record<string, unknown>>(source.get('nodes'))\n if (!nodes) return { ok: false, error: AI_EDIT_NO_NODES_COPY }\n const applied = aiLayoutWithMenuChanges(nodes, input.menus, input.changes)\n if (!applied.nodes) return { ok: false, error: applied.notDone.join(' ') || AI_NAVIGATION_NOTHING_COPY }\n const packed = encodeStoredNodes(applied.nodes as never)\n if (!packed) return { ok: false, error: AI_EDIT_NO_NODES_COPY }\n // A copy of the version it started from — everything the editor stored —\n // with the changed menus, as the versions route copies one.\n const copy = { ...(source.data() as Record<string, unknown>) }\n delete copy['aiEditJobId']\n tx.create(newRef, {\n ...copy,\n nodes: Buffer.from(packed),\n displayName: AI_NAVIGATION_VERSION_NAME,\n createdAt: input.now,\n updatedAt: input.now,\n createdBy: input.uid,\n aiJobId: input.aiJobId,\n })\n return { ok: true, versionId: input.versionId, done: applied.done, notDone: applied.notDone }\n })\n}\n\nexport interface AiJobNavigationStepDeps {\n readTarget?: typeof readAiEditTarget\n readPages?: typeof readAiNavigationSitePages\n write?: typeof writeAiNavigation\n lockdown?: typeof getLockdownVerdict\n}\n\n/** What the step decided before writing: the layout, its version, the change and what was read away. */\ninterface AiNavigationPlan {\n layoutId: string\n menus: AiMenuName[]\n changes: AiMenuChanges\n problems: string[]\n}\n\nasync function navigationPlanOf(\n firestore: Firestore,\n job: Pick<AiJob, 'hostId' | 'inputs'>,\n readPages: typeof readAiNavigationSitePages,\n): Promise<AiNavigationPlan | string> {\n if (!job.hostId) return AI_NAVIGATION_NO_SITE_COPY\n const inputs = job.inputs ?? {}\n const pages = await readPages(firestore, job.hostId)\n const made = buildMadeOf(inputs)\n const layoutId = aiNavigationLayoutOf(text(inputs['layout']), { pages, made })\n if (!layoutId) return AI_NAVIGATION_NO_LAYOUT_COPY\n const read = aiMenuChangesOf(inputs, (destination) => aiNavigationTarget(destination, { pages, made }))\n if (aiMenuChangesEmpty(read.changes)) return [AI_NAVIGATION_NOTHING_COPY, ...read.problems].join(' ')\n return { layoutId, menus: aiMenusOf(inputs['menu']), changes: read.changes, problems: read.problems }\n}\n\nexport function createAiJobNavigationStep(deps: AiJobNavigationStepDeps = {}): AiJobStepRunner {\n const readTarget = deps.readTarget ?? readAiEditTarget\n const readPages = deps.readPages ?? readAiNavigationSitePages\n const write = deps.write ?? writeAiNavigation\n return async ({ job, now, firestore, org, modelFor }): Promise<AiJobStepOutcome> => {\n // No model is asked: the outcome records the edit step's at no cost.\n const model = modelFor?.('job.edit') ?? aiJobEditModel()\n const unspent = (failure: string): AiJobStepOutcome => ({ ...aiUnspentOutcome(model), failure })\n const hostId = job.hostId\n if (!hostId) return unspent(AI_NAVIGATION_NO_SITE_COPY)\n const plan = await navigationPlanOf(firestore, job, readPages)\n if (typeof plan === 'string') return unspent(plan)\n const versionId = aiEditVersionId(job)\n const read = await readTarget(firestore, {\n orgId: job.orgId,\n hostId,\n inputs: { target: plan.layoutId, targetKind: 'layout', versionId: null },\n jobId: versionId,\n })\n if ('error' in read) return unspent(read.error)\n const output = (done: readonly string[], notDone: readonly string[]): AiJobOutput => ({\n resource: 'layout',\n id: read.id,\n versionId,\n hostId,\n hostSubdomain: read.hostSubdomain,\n label: read.name,\n note: aiNavigationOutputNote(done, notDone),\n })\n if (read.written) return aiUnspentOutcome(model, { outputs: [output([], [])] })\n const orgData = org ?? ((await firestore.collection('orgs').doc(job.orgId).get()).data() ?? null)\n const host = await firestore.collection('hosts').doc(hostId).get()\n const refused = await aiNavigationAccessRefusal({ host, uid: job.createdBy, org: orgData }, { lockdown: deps.lockdown })\n if (refused) return aiUnspentOutcome(model, { review: aiLimitReview(refused) })\n const versioning = checkEntitlement(orgData as never, 'versioning')\n const placement = aiEditPlacement({\n versioning,\n served: true,\n sourceIsPointer: read.sourceVersionId === read.pointerVersionId,\n sourceScheduled: read.scheduledVersionId === read.sourceVersionId,\n })\n if (placement === 'refused') return aiUnspentOutcome(model, { review: aiLimitReview(AI_EDIT_NEEDS_VERSIONING_COPY) })\n const written = await write(firestore, {\n hostId,\n layoutId: read.id,\n sourceVersionId: read.sourceVersionId,\n versionId,\n menus: plan.menus,\n changes: plan.changes,\n uid: job.createdBy,\n aiJobId: aiOriginJobId(job),\n versioning,\n now,\n })\n if (written.ok === false) return unspent(written.error)\n return aiUnspentOutcome(model, { outputs: [output(written.done, [...written.notDone, ...plan.problems])] })\n }\n}\n\nexport const runAiJobNavigationStep = createAiJobNavigationStep()\n\n// ── Admission ─────────────────────────────────────────────────────────────\n\nexport interface AiNavigationJobAdmissionDeps {\n ownerOf?: typeof resolveOrgIdForHost\n readTarget?: typeof readAiEditTarget\n readPages?: typeof readAiNavigationSitePages\n lockdown?: typeof getLockdownVerdict\n}\n\n/**\n * A navigation change is admitted on a site of the job's own org, for a\n * member who may write its content now, where the site has a layout and the\n * change has somewhere to land that no visitor sees on this plan. Refused in\n * those words before anything exists.\n */\nexport function createAiNavigationJobAdmission(deps: AiNavigationJobAdmissionDeps = {}): AiJobAdmission {\n const ownerOf = deps.ownerOf ?? resolveOrgIdForHost\n const readTarget = deps.readTarget ?? readAiEditTarget\n const readPages = deps.readPages ?? readAiNavigationSitePages\n return async (context): Promise<AiJobAdmissionRefusal | null> => {\n if (!context.hostId) return { status: 400, error: AI_NAVIGATION_NO_SITE_COPY }\n const owner = await ownerOf(context.hostId)\n if (!owner || owner !== context.orgId) return { status: 404, error: 'Unknown site' }\n const host = await context.firestore.collection('hosts').doc(context.hostId).get()\n const refused = await aiNavigationAccessRefusal({ host, uid: context.uid ?? '', org: context.org }, { lockdown: deps.lockdown })\n if (refused) return { status: 403, error: refused }\n const plan = await navigationPlanOf(context.firestore as Firestore, { hostId: context.hostId, inputs: context.inputs as Record<string, unknown> }, readPages)\n if (typeof plan === 'string') return { status: 400, error: plan }\n const read = await readTarget(context.firestore, {\n orgId: context.orgId,\n hostId: context.hostId,\n inputs: { target: plan.layoutId, targetKind: 'layout', versionId: null },\n jobId: null,\n })\n if ('error' in read) return read\n const placement = aiEditPlacement({\n versioning: checkEntitlement(context.org as never, 'versioning'),\n served: true,\n sourceIsPointer: read.sourceVersionId === read.pointerVersionId,\n sourceScheduled: read.scheduledVersionId === read.sourceVersionId,\n })\n return placement === 'refused' ? { status: 403, error: AI_EDIT_NEEDS_VERSIONING_COPY } : null\n }\n}\n\nexport const aiNavigationJobAdmission = createAiNavigationJobAdmission()\n\n/** Registers the navigation step and its door (AGL-3728). */\nexport function registerAiNavigationJob(): void {\n registerAiJobStep(AI_NAVIGATION_KIND, runAiJobNavigationStep, { minimumMs: AI_JOB_NAVIGATION_STEP_MINIMUM_MS })\n registerAiJobAdmission(AI_NAVIGATION_KIND, aiNavigationJobAdmission)\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","decodeStoredNodes","encodeStoredNodes","getLockdownVerdict","resolveOrgIdForHost","AI_EDIT_GONE_COPY","AI_EDIT_NEEDS_VERSIONING_COPY","AI_EDIT_NO_NODES_COPY","AI_EDIT_VERSION_GONE_COPY","isAiPlanNewRef","AI_BUILD_MADE_INPUT","aiBuildStoreLayoutOf","aiLayoutWithMenuChanges","aiMenuChangesEmpty","aiMenuChangesOf","aiMenusOf","registerAiJobAdmission","aiOriginJobId","aiEditPlacement","aiEditVersionId","aiJobEditModel","readAiEditTarget","aiLimitReview","aiUnspentOutcome","registerAiJobStep","AI_NAVIGATION_KIND","AI_JOB_NAVIGATION_STEP_MINIMUM_MS","AI_NAVIGATION_NO_SITE_COPY","AI_NAVIGATION_ROLE_COPY","AI_NAVIGATION_LOCKED_COPY","AI_NAVIGATION_NO_LAYOUT_COPY","AI_NAVIGATION_NOTHING_COPY","AI_NAVIGATION_VERSION_NAME","text","value","trim","SITE_PATH","SCREEN_ID","buildMadeOf","inputs","raw","rows","name","Array","isArray","flatMap","entry","row","test","id","pages","layouts","readAiNavigationSitePages","firestore","hostId","snapshot","collection","doc","select","get","docs","kind","layoutId","slug","String","title","template","aiNavigationTarget","destination","context","pageIds","Set","filter","page","map","slice","indexOf","toLowerCase","made","find","has","screenId","replace","startsWith","href","aiNavigationLayoutOf","named","layout","aiNavigationAccessRefusal","input","deps","roles","host","uid","locked","lockdown","staff","org","data","intent","aiNavigationOutputNote","done","notDone","did","length","join","not","writeAiNavigation","hostRef","docRef","sourceRef","sourceVersionId","newRef","versionId","runTransaction","tx","source","already","Promise","all","exists","ok","error","pointer","placement","versioning","served","sourceIsPointer","sourceScheduled","nodes","applied","menus","changes","packed","copy","create","Buffer","from","displayName","createdAt","now","updatedAt","createdBy","aiJobId","navigationPlanOf","job","readPages","read","problems","createAiJobNavigationStep","readTarget","write","modelFor","model","unspent","failure","plan","orgId","target","targetKind","jobId","output","resource","hostSubdomain","label","note","written","outputs","orgData","refused","review","pointerVersionId","scheduledVersionId","runAiJobNavigationStep","createAiNavigationJobAdmission","ownerOf","status","owner","aiNavigationJobAdmission","registerAiNavigationJob","minimumMs"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,gBAAgB,QAAQ,2CAA0C;AAC3E,SAASC,iBAAiB,EAAEC,iBAAiB,QAAQ,sCAAqC;AAC1F,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SAASC,mBAAmB,QAAQ,gDAA+C;AAEnF,SAASC,iBAAiB,EAAEC,6BAA6B,EAAEC,qBAAqB,EAAEC,yBAAyB,QAAQ,0BAAsB;AACzI,SAASC,cAAc,QAAQ,4BAAwB;AACvD,SAASC,mBAAmB,QAA0B,2BAAuB;AAC7E,SAASC,oBAAoB,QAAQ,mCAA+B;AAEpE,SACEC,uBAAuB,EACvBC,kBAAkB,EAClBC,eAAe,EACfC,SAAS,QAIJ,8BAA0B;AACjC,SAASC,sBAAsB,QAAyD,wBAAoB;AAC5G,SAASC,aAAa,QAAQ,wBAAoB;AAClD,SAASC,eAAe,EAAEC,eAAe,EAAEC,cAAc,EAAEC,gBAAgB,QAAQ,wBAAoB;AACvG,SAASC,aAAa,EAAEC,gBAAgB,QAAQ,yBAAqB;AAErE,SAASC,iBAAiB,QAAQ,eAAW;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCC,GAED,OAAO,MAAMC,qBAAqB,aAAqB;AAEvD;;;;CAIC,GACD,OAAO,MAAMC,oCAAoC,KAAK;AAEtD,OAAO,MAAMC,6BAA6B,0CAAyC;AACnF,OAAO,MAAMC,0BAA0B,iFAAgF;AACvH,OAAO,MAAMC,4BAA4B,gDAA+C;AACxF,OAAO,MAAMC,+BAA+B,wFAAuF;AACnI,OAAO,MAAMC,6BAA6B,oDAAmD;AAE7F,+DAA+D,GAC/D,OAAO,MAAMC,6BAA6B,eAAc;AAExD,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,MAAMC,IAAI,KAAK;AAErF,oDAAoD,GACpD,MAAMC,YAAY;AAClB,MAAMC,YAAY;AAElB,SAASC,YAAYC,MAA4D;;IAC/E,MAAMC,cAAOD,0BAAAA,MAAQ,CAAC7B,oBAAoB,mBAAI,CAAC;IAC/C,MAAM+B,OAAO,CAACP,OAAgBQ,OAC5B,AAACC,CAAAA,MAAMC,OAAO,CAACV,SAASA,QAAQ,EAAE,AAAD,EAAGW,OAAO,CAAC,CAACC;YAC3C,MAAMC,MAAOD,gBAAAA,QAAS,CAAC;YACvB,OAAOb,KAAKc,GAAG,CAACL,KAAK,KAAKL,UAAUW,IAAI,CAACf,KAAKc,GAAG,CAAC,KAAK,KAAK;gBAAC;oBAAE,CAACL,KAAK,EAAET,KAAKc,GAAG,CAACL,KAAK;oBAAGO,IAAIhB,KAAKc,GAAG,CAAC,KAAK;gBAAE;aAAE,GAAG,EAAE;QACrH;IACF,OAAO;QAAEG,OAAOT,KAAKD,IAAIU,KAAK,EAAE;QAAkCC,SAASV,KAAKD,IAAIW,OAAO,EAAE;IAAkC;AACjI;AAWA,wDAAwD,GACxD,OAAO,eAAeC,0BAA0BC,SAAoB,EAAEC,MAAc;IAClF,MAAMC,WAAW,MAAMF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACH,QAAQE,UAAU,CAAC,WAAWE,MAAM,CAAC,eAAe,QAAQ,QAAQ,aAAa,YAAYC,GAAG;IACzJ,OAAOJ,SAASK,IAAI,CAACf,OAAO,CAAC,CAACY;YAOXA,UACCA;QAPlB,MAAMI,OAAOJ,IAAIE,GAAG,CAAC;QACrB,IAAIF,IAAIE,GAAG,CAAC,gBAAiBE,QAAQA,SAAS,YAAa,OAAO,EAAE;QACpE,MAAMC,WAAWL,IAAIE,GAAG,CAAC;QACzB,OAAO;YACL;gBACEV,IAAIQ,IAAIR,EAAE;gBACVc,MAAMC,QAAOP,WAAAA,IAAIE,GAAG,CAAC,mBAARF,WAAmB;gBAChCQ,OAAOD,QAAOP,YAAAA,IAAIE,GAAG,CAAC,0BAARF,YAA0B;gBACxCS,UAAUL,SAAS;gBACnBC,UAAU,OAAOA,aAAa,YAAYA,WAAWA,WAAW;YAClE;SACD;IACH;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASK,mBACdC,WAAmB,EACnBC,OAA+F;IAE/F,MAAMnC,QAAQkC,YAAYjC,IAAI;IAC9B,MAAMmC,UAAU,IAAIC,IAAIF,QAAQnB,KAAK,CAACsB,MAAM,CAAC,CAACC,OAAS,CAACA,KAAKP,QAAQ,EAAEQ,GAAG,CAAC,CAACD,OAASA,KAAKxB,EAAE;IAC5F,IAAIxC,eAAeyB,QAAyB;QAC1C,MAAM+B,QAAQ/B,MAAMyC,KAAK,CAACzC,MAAM0C,OAAO,CAAC,OAAO,GAAGzC,IAAI,GAAG0C,WAAW;QACpE,MAAMC,OAAOT,QAAQS,IAAI,CAAC5B,KAAK,CAAC6B,IAAI,CAAC,CAACN,OAASA,KAAKR,KAAK,CAAC9B,IAAI,GAAG0C,WAAW,OAAOZ;QACnF,+DAA+D;QAC/D,OAAOa,QAAQR,QAAQU,GAAG,CAACF,KAAK7B,EAAE,IAAI;YAAEgC,UAAUH,KAAK7B,EAAE;QAAC,IAAI;IAChE;IACA,MAAMA,KAAKf,MAAMgD,OAAO,CAAC,WAAW;IACpC,IAAIZ,QAAQU,GAAG,CAAC/B,KAAK,OAAO;QAAEgC,UAAUhC;IAAG;IAC3C,IAAIf,MAAMiD,UAAU,CAAC,QAAQ,CAACjD,MAAMiD,UAAU,CAAC,SAAS/C,UAAUY,IAAI,CAACd,QAAQ,OAAO;QAAEkD,MAAMlD;IAAM;IACpG,IAAI,iDAAiDc,IAAI,CAACd,QAAQ,OAAO;QAAEkD,MAAMlD;IAAM;IACvF,OAAO;AACT;AAEA,+FAA+F,GAC/F,OAAO,SAASmD,qBACdC,KAAa,EACbjB,OAAsE;IAEtE,IAAI5D,eAAe6E,QAAyB;;YAEnCjB;QADP,MAAM3B,OAAO4C,MAAMX,KAAK,CAACW,MAAMV,OAAO,CAAC,OAAO,GAAGzC,IAAI,GAAG0C,WAAW;QACnE,gBAAOR,6BAAAA,QAAQS,IAAI,CAAC3B,OAAO,CAAC4B,IAAI,CAAC,CAACQ,SAAWA,OAAO7C,IAAI,CAACP,IAAI,GAAG0C,WAAW,OAAOnC,0BAA3E2B,2BAAkFpB,EAAE,mBAAI;IACjG;IACA,IAAIZ,UAAUW,IAAI,CAACsC,QAAQ,OAAOA;IAClC,OAAO3E,qBAAqB,MAAM0D,QAAQnB,KAAK;AACjD;AAEA,qEAAqE,GACrE,OAAO,eAAesC,0BACpBC,KAAoF,EACpFC,OAAiD,CAAC,CAAC;QAEpCD,iBAEOC;IAFtB,MAAMC,SAASF,kBAAAA,MAAMG,IAAI,CAACjC,GAAG,CAAC,0BAAf8B,kBAAiC,CAAC;IACjD,IAAI,CAAC1F,iBAAiB4F,KAAK,CAACF,MAAMI,GAAG,CAAC,GAAG,OAAOjE;IAChD,MAAMkE,SAAS,MAAM,EAACJ,iBAAAA,KAAKK,QAAQ,YAAbL,iBAAiBvF,oBAAoB;QACzD6F,OAAO;QACPH,KAAKJ,MAAMI,GAAG;QACdI,KAAKR,MAAMQ,GAAG;QACdL,MAAMH,MAAMG,IAAI,CAACM,IAAI;QACrBC,QAAQ;IACV;IACA,OAAOL,SAASjE,4BAA4B;AAC9C;AAEA,mGAAmG,GACnG,OAAO,SAASuE,uBAAuBC,IAAuB,EAAEC,OAA0B;IACxF,MAAMC,MAAMF,KAAKG,MAAM,GAAG,GAAGH,KAAKI,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG;IACjD,MAAMC,MAAMJ,QAAQE,MAAM,GAAG,CAAC,WAAW,EAAEF,QAAQG,IAAI,CAAC,MAAM,GAAG;IACjE,OAAO,GAAGF,IAAI,wHAAwH,EAAEG,KAAK,CAAC/B,KAAK,CAAC,GAAG;AACzJ;AAMA;;;;;CAKC,GACD,OAAO,eAAegC,kBACpBtD,SAAoB,EACpBoC,KAWC;IAED,MAAMmB,UAAUvD,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACgC,MAAMnC,MAAM;IAC9D,MAAMuD,SAASD,QAAQpD,UAAU,CAAC,WAAWC,GAAG,CAACgC,MAAM3B,QAAQ;IAC/D,MAAMgD,YAAYD,OAAOrD,UAAU,CAAC,YAAYC,GAAG,CAACgC,MAAMsB,eAAe;IACzE,MAAMC,SAASH,OAAOrD,UAAU,CAAC,YAAYC,GAAG,CAACgC,MAAMwB,SAAS;IAChE,OAAO5D,UAAU6D,cAAc,CAAC,OAAOC;YAKtBvB;QAJf,MAAM,CAACA,MAAMnC,KAAK2D,QAAQC,QAAQ,GAAG,MAAMC,QAAQC,GAAG,CAAC;YAACJ,GAAGxD,GAAG,CAACiD;YAAUO,GAAGxD,GAAG,CAACkD;YAASM,GAAGxD,GAAG,CAACmD;YAAYK,GAAGxD,GAAG,CAACqD;SAAQ;QAC3H,IAAIK,QAAQG,MAAM,EAAE,OAAO;YAAEC,IAAI;YAAMR,WAAWxB,MAAMwB,SAAS;YAAEZ,MAAM,EAAE;YAAEC,SAAS,EAAE;QAAC;QACzF,IAAI,CAAC7C,IAAI+D,MAAM,IAAI/D,IAAIE,GAAG,CAAC,gBAAgB,MAAM,OAAO;YAAE8D,IAAI;YAAOC,OAAOrH;QAAkB;QAC9F,IAAI,CAAC+G,OAAOI,MAAM,EAAE,OAAO;YAAEC,IAAI;YAAOC,OAAOlH;QAA0B;QACzE,MAAMmF,SAASC,YAAAA,KAAKjC,GAAG,CAAC,0BAATiC,YAA2B,CAAC;QAC3C,IAAI,CAAC7F,iBAAiB4F,KAAK,CAACF,MAAMI,GAAG,CAAC,GAAG,OAAO;YAAE4B,IAAI;YAAOC,OAAO9F;QAAwB;QAC5F,MAAM+F,UAAU1F,KAAKwB,IAAIE,GAAG,CAAC,iBAAiB;QAC9C,MAAMiE,YAAY1G,gBAAgB;YAChC2G,YAAYpC,MAAMoC,UAAU;YAC5BC,QAAQ;YACRC,iBAAiBJ,YAAYlC,MAAMsB,eAAe;YAClDiB,iBAAiB;QACnB;QACA,IAAIJ,cAAc,WAAW,OAAO;YAAEH,IAAI;YAAOC,OAAOpH;QAA8B;QACtF,MAAM2H,QAAQhI,kBAA2CmH,OAAOzD,GAAG,CAAC;QACpE,IAAI,CAACsE,OAAO,OAAO;YAAER,IAAI;YAAOC,OAAOnH;QAAsB;QAC7D,MAAM2H,UAAUtH,wBAAwBqH,OAAOxC,MAAM0C,KAAK,EAAE1C,MAAM2C,OAAO;QACzE,IAAI,CAACF,QAAQD,KAAK,EAAE,OAAO;YAAER,IAAI;YAAOC,OAAOQ,QAAQ5B,OAAO,CAACG,IAAI,CAAC,QAAQ1E;QAA2B;QACvG,MAAMsG,SAASnI,kBAAkBgI,QAAQD,KAAK;QAC9C,IAAI,CAACI,QAAQ,OAAO;YAAEZ,IAAI;YAAOC,OAAOnH;QAAsB;QAC9D,yEAAyE;QACzE,4DAA4D;QAC5D,MAAM+H,OAAO,aAAMlB,OAAOlB,IAAI;QAC9B,OAAOoC,IAAI,CAAC,cAAc;QAC1BnB,GAAGoB,MAAM,CAACvB,QAAQ,aACbsB;YACHL,OAAOO,OAAOC,IAAI,CAACJ;YACnBK,aAAa1G;YACb2G,WAAWlD,MAAMmD,GAAG;YACpBC,WAAWpD,MAAMmD,GAAG;YACpBE,WAAWrD,MAAMI,GAAG;YACpBkD,SAAStD,MAAMsD,OAAO;;QAExB,OAAO;YAAEtB,IAAI;YAAMR,WAAWxB,MAAMwB,SAAS;YAAEZ,MAAM6B,QAAQ7B,IAAI;YAAEC,SAAS4B,QAAQ5B,OAAO;QAAC;IAC9F;AACF;AAiBA,eAAe0C,iBACb3F,SAAoB,EACpB4F,GAAqC,EACrCC,SAA2C;QAG5BD;IADf,IAAI,CAACA,IAAI3F,MAAM,EAAE,OAAO3B;IACxB,MAAMY,UAAS0G,cAAAA,IAAI1G,MAAM,YAAV0G,cAAc,CAAC;IAC9B,MAAM/F,QAAQ,MAAMgG,UAAU7F,WAAW4F,IAAI3F,MAAM;IACnD,MAAMwB,OAAOxC,YAAYC;IACzB,MAAMuB,WAAWuB,qBAAqBpD,KAAKM,MAAM,CAAC,SAAS,GAAG;QAAEW;QAAO4B;IAAK;IAC5E,IAAI,CAAChB,UAAU,OAAOhC;IACtB,MAAMqH,OAAOrI,gBAAgByB,QAAQ,CAAC6B,cAAgBD,mBAAmBC,aAAa;YAAElB;YAAO4B;QAAK;IACpG,IAAIjE,mBAAmBsI,KAAKf,OAAO,GAAG,OAAO;QAACrG;WAA+BoH,KAAKC,QAAQ;KAAC,CAAC3C,IAAI,CAAC;IACjG,OAAO;QAAE3C;QAAUqE,OAAOpH,UAAUwB,MAAM,CAAC,OAAO;QAAG6F,SAASe,KAAKf,OAAO;QAAEgB,UAAUD,KAAKC,QAAQ;IAAC;AACtG;AAEA,OAAO,SAASC,0BAA0B3D,OAAgC,CAAC,CAAC;QACvDA,kBACDA,iBACJA;IAFd,MAAM4D,cAAa5D,mBAAAA,KAAK4D,UAAU,YAAf5D,mBAAmBrE;IACtC,MAAM6H,aAAYxD,kBAAAA,KAAKwD,SAAS,YAAdxD,kBAAkBtC;IACpC,MAAMmG,SAAQ7D,cAAAA,KAAK6D,KAAK,YAAV7D,cAAciB;IAC5B,OAAO,OAAO,EAAEsC,GAAG,EAAEL,GAAG,EAAEvF,SAAS,EAAE4C,GAAG,EAAEuD,QAAQ,EAAE;kBA0B1B;QAzBxB,qEAAqE;QACrE,MAAMC,gBAAQD,4BAAAA,SAAW,8BAAepI;QACxC,MAAMsI,UAAU,CAACC,UAAuC,aAAKpI,iBAAiBkI;gBAAQE;;QACtF,MAAMrG,SAAS2F,IAAI3F,MAAM;QACzB,IAAI,CAACA,QAAQ,OAAOoG,QAAQ/H;QAC5B,MAAMiI,OAAO,MAAMZ,iBAAiB3F,WAAW4F,KAAKC;QACpD,IAAI,OAAOU,SAAS,UAAU,OAAOF,QAAQE;QAC7C,MAAM3C,YAAY9F,gBAAgB8H;QAClC,MAAME,OAAO,MAAMG,WAAWjG,WAAW;YACvCwG,OAAOZ,IAAIY,KAAK;YAChBvG;YACAf,QAAQ;gBAAEuH,QAAQF,KAAK9F,QAAQ;gBAAEiG,YAAY;gBAAU9C,WAAW;YAAK;YACvE+C,OAAO/C;QACT;QACA,IAAI,WAAWkC,MAAM,OAAOO,QAAQP,KAAKzB,KAAK;QAC9C,MAAMuC,SAAS,CAAC5D,MAAyBC,UAA6C,CAAA;gBACpF4D,UAAU;gBACVjH,IAAIkG,KAAKlG,EAAE;gBACXgE;gBACA3D;gBACA6G,eAAehB,KAAKgB,aAAa;gBACjCC,OAAOjB,KAAKzG,IAAI;gBAChB2H,MAAMjE,uBAAuBC,MAAMC;YACrC,CAAA;QACA,IAAI6C,KAAKmB,OAAO,EAAE,OAAO/I,iBAAiBkI,OAAO;YAAEc,SAAS;gBAACN,OAAO,EAAE,EAAE,EAAE;aAAE;QAAC;QAC7E,MAAMO,UAAUvE,cAAAA,OAAQ,QAAA,AAAC,CAAA,MAAM5C,UAAUG,UAAU,CAAC,QAAQC,GAAG,CAACwF,IAAIY,KAAK,EAAElG,GAAG,EAAC,EAAGuC,IAAI,cAA9D,QAAoE;QAC5F,MAAMN,OAAO,MAAMvC,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACH,QAAQK,GAAG;QAChE,MAAM8G,UAAU,MAAMjF,0BAA0B;YAAEI;YAAMC,KAAKoD,IAAIH,SAAS;YAAE7C,KAAKuE;QAAQ,GAAG;YAAEzE,UAAUL,KAAKK,QAAQ;QAAC;QACtH,IAAI0E,SAAS,OAAOlJ,iBAAiBkI,OAAO;YAAEiB,QAAQpJ,cAAcmJ;QAAS;QAC7E,MAAM5C,aAAa7H,iBAAiBwK,SAAkB;QACtD,MAAM5C,YAAY1G,gBAAgB;YAChC2G;YACAC,QAAQ;YACRC,iBAAiBoB,KAAKpC,eAAe,KAAKoC,KAAKwB,gBAAgB;YAC/D3C,iBAAiBmB,KAAKyB,kBAAkB,KAAKzB,KAAKpC,eAAe;QACnE;QACA,IAAIa,cAAc,WAAW,OAAOrG,iBAAiBkI,OAAO;YAAEiB,QAAQpJ,cAAchB;QAA+B;QACnH,MAAMgK,UAAU,MAAMf,MAAMlG,WAAW;YACrCC;YACAQ,UAAUqF,KAAKlG,EAAE;YACjB8D,iBAAiBoC,KAAKpC,eAAe;YACrCE;YACAkB,OAAOyB,KAAKzB,KAAK;YACjBC,SAASwB,KAAKxB,OAAO;YACrBvC,KAAKoD,IAAIH,SAAS;YAClBC,SAAS9H,cAAcgI;YACvBpB;YACAe;QACF;QACA,IAAI0B,QAAQ7C,EAAE,KAAK,OAAO,OAAOiC,QAAQY,QAAQ5C,KAAK;QACtD,OAAOnG,iBAAiBkI,OAAO;YAAEc,SAAS;gBAACN,OAAOK,QAAQjE,IAAI,EAAE;uBAAIiE,QAAQhE,OAAO;uBAAKsD,KAAKR,QAAQ;iBAAC;aAAE;QAAC;IAC3G;AACF;AAEA,OAAO,MAAMyB,yBAAyBxB,4BAA2B;AAWjE;;;;;CAKC,GACD,OAAO,SAASyB,+BAA+BpF,OAAqC,CAAC,CAAC;QACpEA,eACGA,kBACDA;IAFlB,MAAMqF,WAAUrF,gBAAAA,KAAKqF,OAAO,YAAZrF,gBAAgBtF;IAChC,MAAMkJ,cAAa5D,mBAAAA,KAAK4D,UAAU,YAAf5D,mBAAmBrE;IACtC,MAAM6H,aAAYxD,kBAAAA,KAAKwD,SAAS,YAAdxD,kBAAkBtC;IACpC,OAAO,OAAOiB;YAKiDA;QAJ7D,IAAI,CAACA,QAAQf,MAAM,EAAE,OAAO;YAAE0H,QAAQ;YAAKtD,OAAO/F;QAA2B;QAC7E,MAAMsJ,QAAQ,MAAMF,QAAQ1G,QAAQf,MAAM;QAC1C,IAAI,CAAC2H,SAASA,UAAU5G,QAAQwF,KAAK,EAAE,OAAO;YAAEmB,QAAQ;YAAKtD,OAAO;QAAe;QACnF,MAAM9B,OAAO,MAAMvB,QAAQhB,SAAS,CAACG,UAAU,CAAC,SAASC,GAAG,CAACY,QAAQf,MAAM,EAAEK,GAAG;QAChF,MAAM8G,UAAU,MAAMjF,0BAA0B;YAAEI;YAAMC,GAAG,GAAExB,eAAAA,QAAQwB,GAAG,YAAXxB,eAAe;YAAI4B,KAAK5B,QAAQ4B,GAAG;QAAC,GAAG;YAAEF,UAAUL,KAAKK,QAAQ;QAAC;QAC9H,IAAI0E,SAAS,OAAO;YAAEO,QAAQ;YAAKtD,OAAO+C;QAAQ;QAClD,MAAMb,OAAO,MAAMZ,iBAAiB3E,QAAQhB,SAAS,EAAe;YAAEC,QAAQe,QAAQf,MAAM;YAAEf,QAAQ8B,QAAQ9B,MAAM;QAA4B,GAAG2G;QACnJ,IAAI,OAAOU,SAAS,UAAU,OAAO;YAAEoB,QAAQ;YAAKtD,OAAOkC;QAAK;QAChE,MAAMT,OAAO,MAAMG,WAAWjF,QAAQhB,SAAS,EAAE;YAC/CwG,OAAOxF,QAAQwF,KAAK;YACpBvG,QAAQe,QAAQf,MAAM;YACtBf,QAAQ;gBAAEuH,QAAQF,KAAK9F,QAAQ;gBAAEiG,YAAY;gBAAU9C,WAAW;YAAK;YACvE+C,OAAO;QACT;QACA,IAAI,WAAWb,MAAM,OAAOA;QAC5B,MAAMvB,YAAY1G,gBAAgB;YAChC2G,YAAY7H,iBAAiBqE,QAAQ4B,GAAG,EAAW;YACnD6B,QAAQ;YACRC,iBAAiBoB,KAAKpC,eAAe,KAAKoC,KAAKwB,gBAAgB;YAC/D3C,iBAAiBmB,KAAKyB,kBAAkB,KAAKzB,KAAKpC,eAAe;QACnE;QACA,OAAOa,cAAc,YAAY;YAAEoD,QAAQ;YAAKtD,OAAOpH;QAA8B,IAAI;IAC3F;AACF;AAEA,OAAO,MAAM4K,2BAA2BJ,iCAAgC;AAExE,2DAA2D,GAC3D,OAAO,SAASK;IACd3J,kBAAkBC,oBAAoBoJ,wBAAwB;QAAEO,WAAW1J;IAAkC;IAC7GV,uBAAuBS,oBAAoByJ;AAC7C"}
|
|
@@ -114,7 +114,8 @@ export declare function aiLayoutPageTargets(input: {
|
|
|
114
114
|
* (AGL-3616): a records listing is kept only where the site has the dataset —
|
|
115
115
|
* the tree check admits a repeat over no other, and a card left repeating
|
|
116
116
|
* over nothing would show its `{{item.*}}` — and one that names no fields
|
|
117
|
-
* takes the inventory's, in the dataset's order.
|
|
117
|
+
* takes the inventory's, in the dataset's order. Its cards link the record
|
|
118
|
+
* pages the plan makes, else those the site already serves (AGL-3747).
|
|
118
119
|
*/
|
|
119
120
|
export declare function aiLayoutListingsWithDatasets(listings: readonly AiLayoutListing[], inventory: Pick<AiSiteInventory, 'datasets'> | null): AiLayoutListing[];
|
|
120
121
|
/**
|
|
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
19
19
|
import { aiHomeScreenIds } from "../model/ai-site-inventory.js";
|
|
20
20
|
import { AI_LAYOUT_POST_ADDRESS_TOKENS, aiCompileLayoutPage } from "../layout-language/ai-layout-compiler.js";
|
|
21
|
-
import { aiLayoutListingAt, aiLayoutListingsOf } from "../layout-language/ai-layout-listings.js";
|
|
21
|
+
import { aiLayoutListingAt, aiLayoutListingsOf, aiLayoutRecordHref } from "../layout-language/ai-layout-listings.js";
|
|
22
22
|
import { AI_LAYOUT_LANGUAGE_TEXT, AI_LAYOUT_PAGE_TOOL, aiReadLayoutPage } from "../layout-language/ai-layout-language.js";
|
|
23
23
|
import { aiLayoutCopyCheck } from "../layout-language/ai-layout-gaps.js";
|
|
24
24
|
import { aiLayoutStoredTree } from "../layout-language/ai-layout-store.js";
|
|
@@ -169,10 +169,11 @@ export const AI_JOB_PAGE_LANGUAGE_BUDGET = aiJobStepBudget({
|
|
|
169
169
|
* (AGL-3616): a records listing is kept only where the site has the dataset —
|
|
170
170
|
* the tree check admits a repeat over no other, and a card left repeating
|
|
171
171
|
* over nothing would show its `{{item.*}}` — and one that names no fields
|
|
172
|
-
* takes the inventory's, in the dataset's order.
|
|
172
|
+
* takes the inventory's, in the dataset's order. Its cards link the record
|
|
173
|
+
* pages the plan makes, else those the site already serves (AGL-3747).
|
|
173
174
|
*/ export function aiLayoutListingsWithDatasets(listings, inventory) {
|
|
174
175
|
return listings.flatMap((listing)=>{
|
|
175
|
-
var _listing_imageField;
|
|
176
|
+
var _listing_imageField, _listing_addressField, _listing_recordBase;
|
|
176
177
|
var _listing_fields;
|
|
177
178
|
if (listing.kind !== 'records') return [
|
|
178
179
|
listing
|
|
@@ -181,6 +182,11 @@ export const AI_JOB_PAGE_LANGUAGE_BUDGET = aiJobStepBudget({
|
|
|
181
182
|
if (!row) return [];
|
|
182
183
|
// The photo field is the card's picture, never its words (AGL-3616).
|
|
183
184
|
const imageField = (_listing_imageField = listing.imageField) != null ? _listing_imageField : row.imageField;
|
|
185
|
+
// The address is the card's link to its record's page, never its words (AGL-3727).
|
|
186
|
+
const addressField = (_listing_addressField = listing.addressField) != null ? _listing_addressField : row.addressField;
|
|
187
|
+
// A record page the site already serves for the dataset (AGL-3747), where
|
|
188
|
+
// the plan makes none of its own.
|
|
189
|
+
const recordBase = (_listing_recordBase = listing.recordBase) != null ? _listing_recordBase : row.recordBase;
|
|
184
190
|
const fields = (((_listing_fields = listing.fields) == null ? void 0 : _listing_fields.length) ? listing.fields : row.fields.map((name, index)=>{
|
|
185
191
|
var _row_fieldIds;
|
|
186
192
|
return {
|
|
@@ -188,12 +194,15 @@ export const AI_JOB_PAGE_LANGUAGE_BUDGET = aiJobStepBudget({
|
|
|
188
194
|
name,
|
|
189
195
|
type: 'text'
|
|
190
196
|
};
|
|
191
|
-
})).filter((field)=>field.id !== imageField);
|
|
197
|
+
})).filter((field)=>field.id !== imageField && field.id !== addressField);
|
|
192
198
|
return fields.length ? [
|
|
193
199
|
_extends({}, listing, {
|
|
194
200
|
fields
|
|
195
201
|
}, imageField ? {
|
|
196
202
|
imageField
|
|
203
|
+
} : {}, recordBase && addressField ? {
|
|
204
|
+
recordBase,
|
|
205
|
+
addressField
|
|
197
206
|
} : {})
|
|
198
207
|
] : [];
|
|
199
208
|
});
|
|
@@ -218,7 +227,14 @@ export const AI_JOB_PAGE_LANGUAGE_BUDGET = aiJobStepBudget({
|
|
|
218
227
|
...here.some((listing)=>listing.kind === 'posts') ? AI_LAYOUT_POST_ADDRESS_TOKENS : [],
|
|
219
228
|
...here.flatMap((listing)=>listing.kind === 'records' && listing.imageField ? [
|
|
220
229
|
`{{item.${listing.imageField}}}`
|
|
221
|
-
] : [])
|
|
230
|
+
] : []),
|
|
231
|
+
// Each card's link to its record's own page (AGL-3727), admitted whole.
|
|
232
|
+
...here.flatMap((listing)=>{
|
|
233
|
+
const href = aiLayoutRecordHref(listing);
|
|
234
|
+
return href ? [
|
|
235
|
+
href
|
|
236
|
+
] : [];
|
|
237
|
+
})
|
|
222
238
|
];
|
|
223
239
|
if (!tokens.length) return context;
|
|
224
240
|
return _extends({}, context, {
|