@aglyn/plugins-ai 1.0.0-beta.239 → 1.0.0-beta.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +17 -0
- package/src/lib/components/ai-job-links.js +63 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +8 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +5 -0
- package/src/lib/components/ai-job-requests.js +4 -2
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +7 -1
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
- package/src/lib/components/ai-store-finish-card.component.js +116 -0
- package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
- package/src/lib/components/assist-build-card.component.js +12 -1
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +14 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
- package/src/lib/jobs/ai-job-build-step.js +283 -7
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
- package/src/lib/jobs/ai-job-drafts.js +4 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +11 -4
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-language.js +26 -9
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
- package/src/lib/jobs/ai-job-page-sections.js +53 -5
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-page-step.js +51 -10
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -7
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +89 -2
- package/src/lib/jobs/ai-job-site-datasets.js +239 -15
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
- package/src/lib/jobs/ai-job-site-step.js +109 -22
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
- package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
- package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
- package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
- package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
- package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +18 -0
- package/src/lib/jobs/ai-site-publish.js +76 -15
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.js +25 -2
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -4
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-listings.js +3 -0
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.js +12 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +23 -3
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -0
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-build-store-pages.d.ts +133 -0
- package/src/lib/model/ai-build-store-pages.js +209 -0
- package/src/lib/model/ai-build-store-pages.js.map +1 -0
- package/src/lib/model/ai-layout-store-links.d.ts +64 -0
- package/src/lib/model/ai-layout-store-links.js +165 -0
- package/src/lib/model/ai-layout-store-links.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +5 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +192 -0
- package/src/lib/model/ai-site-store-pages.js +383 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +44 -0
- package/src/lib/runtime/ai-doctrine-validators.js +177 -0
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +3 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +8 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/testing/stand-in-dataset-index.js +8 -4
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { AI_STORE_PAGES, aiStoreFrameLinks, aiStorePagesOfPlan, aiStorePagesToWrite } from "./ai-site-store-pages.js";
|
|
18
|
+
/**
|
|
19
|
+
* A STORE'S OWN PAGES, ADDED BY AN ASSIST BUILD (AGL-3676).
|
|
20
|
+
*
|
|
21
|
+
* A site start that builds a store adds its account, cart and policy pages
|
|
22
|
+
* (`ai-site-store-pages.ts`). An Assist build has no site kind, so a member
|
|
23
|
+
* who asked Assist to "add a shop" got products and a Shop page and nowhere
|
|
24
|
+
* for a shopper to sign in, see a cart or read the store's policies.
|
|
25
|
+
*
|
|
26
|
+
* So a build whose plan turns the site into a store — it makes products, a
|
|
27
|
+
* product page template, or plans a Shop page — gets the same store pages,
|
|
28
|
+
* written by the same code-built unit, behind the same gate (a plan with
|
|
29
|
+
* commerce, Commerce on for the site, an editor). Only the pages the site
|
|
30
|
+
* does not already have: a page of the site's own, or of the plan, that
|
|
31
|
+
* holds one's address or says it is one ("Privacy Policy" at
|
|
32
|
+
* /privacy-policy) stands for it, and nothing is written for that one.
|
|
33
|
+
*
|
|
34
|
+
* The plan keeps what it adds (`AiBuildPlan.storePages`), worked out by code
|
|
35
|
+
* when the plan is kept, never asked of the model; the plan card shows those
|
|
36
|
+
* pages as added by the platform, at no credits, and the build writes them in
|
|
37
|
+
* one unit of its own after every planned unit. The build checks the site
|
|
38
|
+
* again before writing, so a page made since the plan is not made twice.
|
|
39
|
+
*
|
|
40
|
+
* Pure: safe for the console to import.
|
|
41
|
+
*/ /** The ledger slot, and the op, of a build's store pages unit. Persisted; never renamed. */ export const AI_BUILD_STORE_SLOT = 'store';
|
|
42
|
+
export const AI_BUILD_STORE_OP = 'store';
|
|
43
|
+
/** The commerce plugin's product operation (`PRODUCT_AI_OP`), which a build that makes products plans. */ export const AI_BUILD_PRODUCT_OP = 'product';
|
|
44
|
+
/** What the build's row for its store pages reads. */ export const AI_BUILD_STORE_PAGES_LABEL = 'Account, cart and policy pages';
|
|
45
|
+
/** What a build's store pages row says where the site already had every one by the time it ran. */ export const AI_BUILD_STORE_PAGES_PRESENT_NOTE = 'Your site already has its account, cart and policy pages.';
|
|
46
|
+
const firstSegment = (slug)=>slug.trim().replace(/^\/+/, '').split('/')[0].toLowerCase();
|
|
47
|
+
const wordsOf = (page)=>`${page.slug.replace(/[-/_]+/g, ' ')} ${page.title}`;
|
|
48
|
+
/** A planned page whose address or name says it is the store's own: Shop, Store, Products. */ const STORE_PAGE = /\b(shop|store|products?)\b/i;
|
|
49
|
+
/**
|
|
50
|
+
* What a page's words say it is, by store page (beyond its address): a site's
|
|
51
|
+
* "Privacy Policy" at /privacy-policy is its privacy policy, its "Terms &
|
|
52
|
+
* conditions" its terms, its "My account" its account, its "Basket" its cart.
|
|
53
|
+
*/ const PURPOSE = {
|
|
54
|
+
account: /\b(my account|your account|account|sign in|log in|login)\b/i,
|
|
55
|
+
cart: /\b(cart|basket|shopping bag)\b/i,
|
|
56
|
+
shipping: /\b(shipping|delivery|returns?|refunds?)\b/i,
|
|
57
|
+
privacy: /\bprivacy\b/i,
|
|
58
|
+
terms: /\b(terms|conditions)\b/i
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Whether a build's plan turns its site into a store: it makes products, a
|
|
62
|
+
* page template for products, or plans the store's own page (Shop, Store,
|
|
63
|
+
* Products). A page that only names collections or a range is not enough:
|
|
64
|
+
* a gallery has those too.
|
|
65
|
+
*/ export function aiBuildMakesStore(plan) {
|
|
66
|
+
var _plan_items;
|
|
67
|
+
const items = (_plan_items = plan.items) != null ? _plan_items : [];
|
|
68
|
+
if (items.some((item)=>item.op === AI_BUILD_PRODUCT_OP)) return true;
|
|
69
|
+
if (items.some((item)=>{
|
|
70
|
+
var _item_args;
|
|
71
|
+
return item.op === 'template' && ((_item_args = item.args) == null ? void 0 : _item_args['subject']) === 'product';
|
|
72
|
+
})) return true;
|
|
73
|
+
return plan.screens.some((screen)=>!screen.record && firstSegment(screen.slug) !== '' && STORE_PAGE.test(wordsOf(screen)));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The store pages for these pages — the plan's and the site's own together:
|
|
77
|
+
* `aiStorePagesOfPlan`'s answer, and then any store page still to write that
|
|
78
|
+
* a page's words say it already is.
|
|
79
|
+
*/ export function aiStorePagesForSite(pages) {
|
|
80
|
+
const real = pages.filter((page)=>!page.template && firstSegment(page.slug) !== '');
|
|
81
|
+
return aiStorePagesOfPlan(real).map((page)=>{
|
|
82
|
+
if (page.planned) return page;
|
|
83
|
+
const holds = real.find((one)=>PURPOSE[page.key].test(wordsOf(one)));
|
|
84
|
+
if (!holds) return page;
|
|
85
|
+
const slug = firstSegment(holds.slug);
|
|
86
|
+
return _extends({}, page, {
|
|
87
|
+
slug,
|
|
88
|
+
href: `/${slug}`,
|
|
89
|
+
link: holds.title.trim() || page.link,
|
|
90
|
+
planned: holds.title
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const CONTACT = /\b(contact|get in touch|visit us)\b/i;
|
|
95
|
+
/** What a store page's words link, read off the pages a site has or plans: its contact page, its shop and its shipping page. */ export function aiStorePageFactsOf(pages, storePages) {
|
|
96
|
+
var _path, _ref;
|
|
97
|
+
var _storePages_find;
|
|
98
|
+
const real = pages.filter((page)=>!page.template && firstSegment(page.slug) !== '');
|
|
99
|
+
const path = (page)=>page ? `/${firstSegment(page.slug)}` : null;
|
|
100
|
+
return {
|
|
101
|
+
contactPath: path(real.find((page)=>CONTACT.test(wordsOf(page)))),
|
|
102
|
+
shopPath: (_path = path(real.find((page)=>STORE_PAGE.test(wordsOf(page))))) != null ? _path : '/shop',
|
|
103
|
+
shippingPath: (_ref = (_storePages_find = storePages.find((page)=>page.key === 'shipping')) == null ? void 0 : _storePages_find.href) != null ? _ref : '/shipping-returns'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What a build adds for a store (AGL-3676), or `null` where it adds nothing:
|
|
108
|
+
* the plan does not make a store, the site cannot have one (`store` false:
|
|
109
|
+
* no commerce on the plan, or Commerce off for the site), or every store page
|
|
110
|
+
* is already there. The site's own pages come after the plan's, so a page the
|
|
111
|
+
* plan builds at an address is the one linked.
|
|
112
|
+
*/ export function aiBuildStorePagesFor(plan, site) {
|
|
113
|
+
if (!site.store || !aiBuildMakesStore(plan)) return null;
|
|
114
|
+
const all = [
|
|
115
|
+
...plan.screens.map((screen)=>({
|
|
116
|
+
slug: screen.slug,
|
|
117
|
+
title: screen.title,
|
|
118
|
+
template: Boolean(screen.record)
|
|
119
|
+
})),
|
|
120
|
+
...site.pages
|
|
121
|
+
];
|
|
122
|
+
const pages = aiStorePagesForSite(all);
|
|
123
|
+
if (!aiStorePagesToWrite(pages).length) return null;
|
|
124
|
+
return {
|
|
125
|
+
pages,
|
|
126
|
+
facts: aiStorePageFactsOf(all, pages)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The store pages a build still writes once it runs: those its plan kept,
|
|
131
|
+
* less any the site has gained since — by address or by what the page says
|
|
132
|
+
* it is. `[]` when the site now has them all.
|
|
133
|
+
*/ export function aiBuildStorePagesStillMissing(kept, sitePages) {
|
|
134
|
+
const now = aiStorePagesForSite(sitePages);
|
|
135
|
+
return kept.map((page)=>{
|
|
136
|
+
if (page.planned) return page;
|
|
137
|
+
const held = now.find((one)=>one.key === page.key && one.planned);
|
|
138
|
+
return held != null ? held : page;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** Whether a value read off a kept plan is a build's store pages; a plan kept before them carries none. */ export function aiBuildStorePagesOf(plan) {
|
|
142
|
+
const raw = plan == null ? void 0 : plan.storePages;
|
|
143
|
+
if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null;
|
|
144
|
+
const keys = new Set(AI_STORE_PAGES.map((page)=>page.key));
|
|
145
|
+
const pages = raw.pages.filter((page)=>Boolean(page) && keys.has(page.key) && typeof page.href === 'string');
|
|
146
|
+
return pages.length ? {
|
|
147
|
+
pages,
|
|
148
|
+
facts: raw.facts
|
|
149
|
+
} : null;
|
|
150
|
+
}
|
|
151
|
+
/** The store pages a build's plan card lists as added for the store: the ones written, not the ones a page stands for. */ export function aiBuildStorePagesAdded(plan) {
|
|
152
|
+
var _ref;
|
|
153
|
+
var _aiBuildStorePagesOf;
|
|
154
|
+
return aiStorePagesToWrite((_ref = (_aiBuildStorePagesOf = aiBuildStorePagesOf(plan)) == null ? void 0 : _aiBuildStorePagesOf.pages) != null ? _ref : []);
|
|
155
|
+
}
|
|
156
|
+
/** What a header's link to the store's account and cart say (`AI_LAYOUT_ACCOUNT_LINK`). */ export const AI_BUILD_STORE_ACCOUNT_LINK = 'Account';
|
|
157
|
+
export const AI_BUILD_STORE_CART_LINK = 'Cart';
|
|
158
|
+
/**
|
|
159
|
+
* The links a build adds to the site's own layout for these store pages:
|
|
160
|
+
* Account in the header, the cart there too where the header has none, and
|
|
161
|
+
* the account and the policies in the footer — only for the pages that are
|
|
162
|
+
* there, written by the build or stood for by a page the site has.
|
|
163
|
+
*/ export function aiBuildStoreLayoutLinks(pages, written) {
|
|
164
|
+
const there = pages.filter((page)=>page.planned || written.has(page.key));
|
|
165
|
+
const account = there.find((page)=>page.key === 'account');
|
|
166
|
+
const cart = there.find((page)=>page.key === 'cart');
|
|
167
|
+
return {
|
|
168
|
+
header: account ? [
|
|
169
|
+
{
|
|
170
|
+
label: AI_BUILD_STORE_ACCOUNT_LINK,
|
|
171
|
+
href: account.href
|
|
172
|
+
}
|
|
173
|
+
] : [],
|
|
174
|
+
cart: cart ? {
|
|
175
|
+
label: AI_BUILD_STORE_CART_LINK,
|
|
176
|
+
href: cart.href
|
|
177
|
+
} : null,
|
|
178
|
+
footer: aiStoreFrameLinks(there).footer
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** The layout a build's store pages render inside, and its links go in: the Shop page's, else the home page's, else the one most of the site's pages use. */ export function aiBuildStoreLayoutOf(shopLayoutId, sitePages) {
|
|
182
|
+
var _counts_get, _ref;
|
|
183
|
+
var _sort_;
|
|
184
|
+
if (shopLayoutId) return shopLayoutId;
|
|
185
|
+
const pages = sitePages.filter((page)=>!page.template && page.layoutId);
|
|
186
|
+
const home = pages.find((page)=>firstSegment(page.slug) === '');
|
|
187
|
+
if (home == null ? void 0 : home.layoutId) return home.layoutId;
|
|
188
|
+
const counts = new Map();
|
|
189
|
+
for (const page of pages)counts.set(page.layoutId, ((_counts_get = counts.get(page.layoutId)) != null ? _counts_get : 0) + 1);
|
|
190
|
+
return (_ref = (_sort_ = [
|
|
191
|
+
...counts.entries()
|
|
192
|
+
].sort((a, b)=>b[1] - a[1])[0]) == null ? void 0 : _sort_[0]) != null ? _ref : null;
|
|
193
|
+
}
|
|
194
|
+
/** The finish card's step where the site's layout had no list for the store's links: add them by hand. */ export const AI_BUILD_STORE_LINKS_MISSING_STEP = {
|
|
195
|
+
id: 'links',
|
|
196
|
+
title: 'Add Account and policy links to your header and footer',
|
|
197
|
+
text: 'Your header and footer have no list of links to add them to. Add links to your account, cart and policy pages.',
|
|
198
|
+
opens: 'layouts',
|
|
199
|
+
action: 'Open layouts'
|
|
200
|
+
};
|
|
201
|
+
/** The finish card's step where the store's links wait in a draft version of the site's layout: publish it. */ export const AI_BUILD_STORE_LINKS_DRAFT_STEP = {
|
|
202
|
+
id: 'links',
|
|
203
|
+
title: 'Publish your header and footer links',
|
|
204
|
+
text: 'Account and policy links are in a new draft version of your layout. Open it and publish that version so shoppers see them.',
|
|
205
|
+
opens: 'layout',
|
|
206
|
+
action: 'Open the layout'
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
//# sourceMappingURL=ai-build-store-pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-build-store-pages.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 { AiBuildItem, AiBuildPlanScreen } from './ai-build-plan'\nimport type { AiStoreLayoutLinks } from './ai-layout-store-links'\nimport {\n AI_STORE_PAGES,\n aiStoreFrameLinks,\n aiStorePagesOfPlan,\n aiStorePagesToWrite,\n type AiStoreFinishStep,\n type AiStorePageFacts,\n type AiStorePageKey,\n type AiStorePagePlan,\n} from './ai-site-store-pages'\n\n/**\n * A STORE'S OWN PAGES, ADDED BY AN ASSIST BUILD (AGL-3676).\n *\n * A site start that builds a store adds its account, cart and policy pages\n * (`ai-site-store-pages.ts`). An Assist build has no site kind, so a member\n * who asked Assist to \"add a shop\" got products and a Shop page and nowhere\n * for a shopper to sign in, see a cart or read the store's policies.\n *\n * So a build whose plan turns the site into a store — it makes products, a\n * product page template, or plans a Shop page — gets the same store pages,\n * written by the same code-built unit, behind the same gate (a plan with\n * commerce, Commerce on for the site, an editor). Only the pages the site\n * does not already have: a page of the site's own, or of the plan, that\n * holds one's address or says it is one (\"Privacy Policy\" at\n * /privacy-policy) stands for it, and nothing is written for that one.\n *\n * The plan keeps what it adds (`AiBuildPlan.storePages`), worked out by code\n * when the plan is kept, never asked of the model; the plan card shows those\n * pages as added by the platform, at no credits, and the build writes them in\n * one unit of its own after every planned unit. The build checks the site\n * again before writing, so a page made since the plan is not made twice.\n *\n * Pure: safe for the console to import.\n */\n\n/** The ledger slot, and the op, of a build's store pages unit. Persisted; never renamed. */\nexport const AI_BUILD_STORE_SLOT = 'store'\nexport const AI_BUILD_STORE_OP = 'store'\n\n/** The commerce plugin's product operation (`PRODUCT_AI_OP`), which a build that makes products plans. */\nexport const AI_BUILD_PRODUCT_OP = 'product'\n\n/** What the build's row for its store pages reads. */\nexport const AI_BUILD_STORE_PAGES_LABEL = 'Account, cart and policy pages'\n\n/** What a build's store pages row says where the site already had every one by the time it ran. */\nexport const AI_BUILD_STORE_PAGES_PRESENT_NOTE = 'Your site already has its account, cart and policy pages.'\n\n/** What a build adds for a store: its store pages, each written or stood for by a page, and what their words link. */\nexport interface AiBuildStorePages {\n pages: AiStorePagePlan[]\n facts: AiStorePageFacts\n}\n\n/** A page a build reads to decide what a store lacks: a planned page, or one the site has. */\nexport interface AiStoreSitePage {\n /** The site's page id, where it is one of the site's own. */\n id?: string\n slug: string\n title: string\n /** A record template or entry template, which is no page at its own address. */\n template?: boolean\n /** The layout the page renders inside, where it is one of the site's own. */\n layoutId?: string | null\n}\n\nconst firstSegment = (slug: string) => slug.trim().replace(/^\\/+/, '').split('/')[0].toLowerCase()\nconst wordsOf = (page: { slug: string; title: string }) => `${page.slug.replace(/[-/_]+/g, ' ')} ${page.title}`\n\n/** A planned page whose address or name says it is the store's own: Shop, Store, Products. */\nconst STORE_PAGE = /\\b(shop|store|products?)\\b/i\n\n/**\n * What a page's words say it is, by store page (beyond its address): a site's\n * \"Privacy Policy\" at /privacy-policy is its privacy policy, its \"Terms &\n * conditions\" its terms, its \"My account\" its account, its \"Basket\" its cart.\n */\nconst PURPOSE: Readonly<Record<AiStorePageKey, RegExp>> = {\n account: /\\b(my account|your account|account|sign in|log in|login)\\b/i,\n cart: /\\b(cart|basket|shopping bag)\\b/i,\n shipping: /\\b(shipping|delivery|returns?|refunds?)\\b/i,\n privacy: /\\bprivacy\\b/i,\n terms: /\\b(terms|conditions)\\b/i,\n}\n\n/**\n * Whether a build's plan turns its site into a store: it makes products, a\n * page template for products, or plans the store's own page (Shop, Store,\n * Products). A page that only names collections or a range is not enough:\n * a gallery has those too.\n */\nexport function aiBuildMakesStore(plan: {\n screens: ReadonlyArray<Pick<AiBuildPlanScreen, 'slug' | 'title' | 'record'>>\n items?: ReadonlyArray<Pick<AiBuildItem, 'op' | 'args'>>\n}): boolean {\n const items = plan.items ?? []\n if (items.some((item) => item.op === AI_BUILD_PRODUCT_OP)) return true\n if (items.some((item) => item.op === 'template' && item.args?.['subject'] === 'product')) return true\n return plan.screens.some((screen) => !screen.record && firstSegment(screen.slug) !== '' && STORE_PAGE.test(wordsOf(screen)))\n}\n\n/**\n * The store pages for these pages — the plan's and the site's own together:\n * `aiStorePagesOfPlan`'s answer, and then any store page still to write that\n * a page's words say it already is.\n */\nexport function aiStorePagesForSite(pages: readonly AiStoreSitePage[]): AiStorePagePlan[] {\n const real = pages.filter((page) => !page.template && firstSegment(page.slug) !== '')\n return aiStorePagesOfPlan(real).map((page) => {\n if (page.planned) return page\n const holds = real.find((one) => PURPOSE[page.key].test(wordsOf(one)))\n if (!holds) return page\n const slug = firstSegment(holds.slug)\n return { ...page, slug, href: `/${slug}`, link: holds.title.trim() || page.link, planned: holds.title }\n })\n}\n\nconst CONTACT = /\\b(contact|get in touch|visit us)\\b/i\n\n/** What a store page's words link, read off the pages a site has or plans: its contact page, its shop and its shipping page. */\nexport function aiStorePageFactsOf(pages: readonly AiStoreSitePage[], storePages: readonly AiStorePagePlan[]): AiStorePageFacts {\n const real = pages.filter((page) => !page.template && firstSegment(page.slug) !== '')\n const path = (page: AiStoreSitePage | undefined) => (page ? `/${firstSegment(page.slug)}` : null)\n return {\n contactPath: path(real.find((page) => CONTACT.test(wordsOf(page)))),\n shopPath: path(real.find((page) => STORE_PAGE.test(wordsOf(page)))) ?? '/shop',\n shippingPath: storePages.find((page) => page.key === 'shipping')?.href ?? '/shipping-returns',\n }\n}\n\n/**\n * What a build adds for a store (AGL-3676), or `null` where it adds nothing:\n * the plan does not make a store, the site cannot have one (`store` false:\n * no commerce on the plan, or Commerce off for the site), or every store page\n * is already there. The site's own pages come after the plan's, so a page the\n * plan builds at an address is the one linked.\n */\nexport function aiBuildStorePagesFor(\n plan: Parameters<typeof aiBuildMakesStore>[0],\n site: { pages: readonly AiStoreSitePage[]; store: boolean },\n): AiBuildStorePages | null {\n if (!site.store || !aiBuildMakesStore(plan)) return null\n const all: AiStoreSitePage[] = [\n ...plan.screens.map((screen) => ({ slug: screen.slug, title: screen.title, template: Boolean(screen.record) })),\n ...site.pages,\n ]\n const pages = aiStorePagesForSite(all)\n if (!aiStorePagesToWrite(pages).length) return null\n return { pages, facts: aiStorePageFactsOf(all, pages) }\n}\n\n/**\n * The store pages a build still writes once it runs: those its plan kept,\n * less any the site has gained since — by address or by what the page says\n * it is. `[]` when the site now has them all.\n */\nexport function aiBuildStorePagesStillMissing(\n kept: readonly AiStorePagePlan[],\n sitePages: readonly AiStoreSitePage[],\n): AiStorePagePlan[] {\n const now = aiStorePagesForSite(sitePages)\n return kept.map((page) => {\n if (page.planned) return page\n const held = now.find((one) => one.key === page.key && one.planned)\n return held ?? page\n })\n}\n\n/** Whether a value read off a kept plan is a build's store pages; a plan kept before them carries none. */\nexport function aiBuildStorePagesOf(plan: { storePages?: unknown } | null | undefined): AiBuildStorePages | null {\n const raw = plan?.storePages as Partial<AiBuildStorePages> | undefined\n if (!raw || !Array.isArray(raw.pages) || !raw.facts) return null\n const keys = new Set(AI_STORE_PAGES.map((page) => page.key))\n const pages = raw.pages.filter((page): page is AiStorePagePlan => Boolean(page) && keys.has(page.key) && typeof page.href === 'string')\n return pages.length ? { pages, facts: raw.facts as AiStorePageFacts } : null\n}\n\n/** The store pages a build's plan card lists as added for the store: the ones written, not the ones a page stands for. */\nexport function aiBuildStorePagesAdded(plan: { storePages?: unknown } | null | undefined): AiStorePagePlan[] {\n return aiStorePagesToWrite(aiBuildStorePagesOf(plan)?.pages ?? [])\n}\n\n// ── The store's links in the site's own layout ────────────────────────────\n\n/**\n * What a store's links in a layout record on the build's outputs (AGL-3676),\n * as `proposal.storeLinks`: `draft` on the layout version written with them,\n * `missing` where the layout had no header or footer list to add them to.\n */\nexport type AiBuildStoreLinksState = 'draft' | 'missing'\n\n/** What a header's link to the store's account and cart say (`AI_LAYOUT_ACCOUNT_LINK`). */\nexport const AI_BUILD_STORE_ACCOUNT_LINK = 'Account'\nexport const AI_BUILD_STORE_CART_LINK = 'Cart'\n\n/**\n * The links a build adds to the site's own layout for these store pages:\n * Account in the header, the cart there too where the header has none, and\n * the account and the policies in the footer — only for the pages that are\n * there, written by the build or stood for by a page the site has.\n */\nexport function aiBuildStoreLayoutLinks(pages: readonly AiStorePagePlan[], written: ReadonlySet<string>): AiStoreLayoutLinks {\n const there = pages.filter((page) => page.planned || written.has(page.key))\n const account = there.find((page) => page.key === 'account')\n const cart = there.find((page) => page.key === 'cart')\n return {\n header: account ? [{ label: AI_BUILD_STORE_ACCOUNT_LINK, href: account.href }] : [],\n cart: cart ? { label: AI_BUILD_STORE_CART_LINK, href: cart.href } : null,\n footer: aiStoreFrameLinks(there).footer,\n }\n}\n\n/** The layout a build's store pages render inside, and its links go in: the Shop page's, else the home page's, else the one most of the site's pages use. */\nexport function aiBuildStoreLayoutOf(shopLayoutId: string | null, sitePages: readonly AiStoreSitePage[]): string | null {\n if (shopLayoutId) return shopLayoutId\n const pages = sitePages.filter((page) => !page.template && page.layoutId)\n const home = pages.find((page) => firstSegment(page.slug) === '')\n if (home?.layoutId) return home.layoutId\n const counts = new Map<string, number>()\n for (const page of pages) counts.set(page.layoutId as string, (counts.get(page.layoutId as string) ?? 0) + 1)\n return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? null\n}\n\n/** The finish card's step where the site's layout had no list for the store's links: add them by hand. */\nexport const AI_BUILD_STORE_LINKS_MISSING_STEP: AiStoreFinishStep = {\n id: 'links',\n title: 'Add Account and policy links to your header and footer',\n text: 'Your header and footer have no list of links to add them to. Add links to your account, cart and policy pages.',\n opens: 'layouts',\n action: 'Open layouts',\n}\n\n/** The finish card's step where the store's links wait in a draft version of the site's layout: publish it. */\nexport const AI_BUILD_STORE_LINKS_DRAFT_STEP: AiStoreFinishStep = {\n id: 'links',\n title: 'Publish your header and footer links',\n text: 'Account and policy links are in a new draft version of your layout. Open it and publish that version so shoppers see them.',\n opens: 'layout',\n action: 'Open the layout',\n}\n"],"names":["AI_STORE_PAGES","aiStoreFrameLinks","aiStorePagesOfPlan","aiStorePagesToWrite","AI_BUILD_STORE_SLOT","AI_BUILD_STORE_OP","AI_BUILD_PRODUCT_OP","AI_BUILD_STORE_PAGES_LABEL","AI_BUILD_STORE_PAGES_PRESENT_NOTE","firstSegment","slug","trim","replace","split","toLowerCase","wordsOf","page","title","STORE_PAGE","PURPOSE","account","cart","shipping","privacy","terms","aiBuildMakesStore","plan","items","some","item","op","args","screens","screen","record","test","aiStorePagesForSite","pages","real","filter","template","map","planned","holds","find","one","key","href","link","CONTACT","aiStorePageFactsOf","storePages","path","contactPath","shopPath","shippingPath","aiBuildStorePagesFor","site","store","all","Boolean","length","facts","aiBuildStorePagesStillMissing","kept","sitePages","now","held","aiBuildStorePagesOf","raw","Array","isArray","keys","Set","has","aiBuildStorePagesAdded","AI_BUILD_STORE_ACCOUNT_LINK","AI_BUILD_STORE_CART_LINK","aiBuildStoreLayoutLinks","written","there","header","label","footer","aiBuildStoreLayoutOf","shopLayoutId","counts","layoutId","home","Map","set","get","entries","sort","a","b","AI_BUILD_STORE_LINKS_MISSING_STEP","id","text","opens","action","AI_BUILD_STORE_LINKS_DRAFT_STEP"],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A STORE'S LINKS IN A LAYOUT THE SITE ALREADY HAS (AGL-3676).
|
|
19
|
+
*
|
|
20
|
+
* A build that makes a site a store and builds no layout of its own adds its
|
|
21
|
+
* account, cart and policy pages to a site whose header and footer were made
|
|
22
|
+
* before them. So code — never the model — adds the links to that layout:
|
|
23
|
+
* Account (and Cart, where the header has no cart) to the header's links,
|
|
24
|
+
* and the account and policies to the footer's list of links, each a copy of
|
|
25
|
+
* a link already in that list so it looks like its neighbours.
|
|
26
|
+
*
|
|
27
|
+
* Only where the layout has a list to add to: a header (an App Bar, or an
|
|
28
|
+
* element that renders `header`) holding links, or a Toolbar holding one; a
|
|
29
|
+
* footer (an element that renders `footer`) holding a list of two or more
|
|
30
|
+
* links. A layout with neither is left as it is, and the build says what to
|
|
31
|
+
* add by hand. A link already there — by its address, or to the page that
|
|
32
|
+
* holds that address — is never added twice.
|
|
33
|
+
*
|
|
34
|
+
* Pure: nodes in, nodes out.
|
|
35
|
+
*/
|
|
36
|
+
/** One link a store adds to a layout. */
|
|
37
|
+
export interface AiStoreLayoutLink {
|
|
38
|
+
label: string;
|
|
39
|
+
href: string;
|
|
40
|
+
}
|
|
41
|
+
/** What a store adds to a layout: the header's links (Account, Cart) and the footer's (account, policies). */
|
|
42
|
+
export interface AiStoreLayoutLinks {
|
|
43
|
+
header: AiStoreLayoutLink[];
|
|
44
|
+
/** The cart's page, linked from the header only where the layout has no cart of its own. */
|
|
45
|
+
cart: AiStoreLayoutLink | null;
|
|
46
|
+
footer: AiStoreLayoutLink[];
|
|
47
|
+
}
|
|
48
|
+
/** What adding a store's links to a layout came to. */
|
|
49
|
+
export interface AiLayoutStoreLinksResult {
|
|
50
|
+
/** The layout's nodes with the links added, or `null` where nothing was added. */
|
|
51
|
+
nodes: Record<string, unknown> | null;
|
|
52
|
+
/** Whether the layout has a header list and a footer list to add to. */
|
|
53
|
+
header: boolean;
|
|
54
|
+
footer: boolean;
|
|
55
|
+
/** The addresses added, in order. */
|
|
56
|
+
added: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The layout's nodes with a store's links added (AGL-3676), and whether it
|
|
60
|
+
* had a header list and a footer list to add them to. `screenPaths` names
|
|
61
|
+
* the address of each of the site's pages, so a link to a page counts as a
|
|
62
|
+
* link to its address.
|
|
63
|
+
*/
|
|
64
|
+
export declare function aiLayoutWithStoreLinks(nodes: Readonly<Record<string, unknown>>, links: AiStoreLayoutLinks, screenPaths?: Readonly<Record<string, string>>): AiLayoutStoreLinksResult;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* A STORE'S LINKS IN A LAYOUT THE SITE ALREADY HAS (AGL-3676).
|
|
18
|
+
*
|
|
19
|
+
* A build that makes a site a store and builds no layout of its own adds its
|
|
20
|
+
* account, cart and policy pages to a site whose header and footer were made
|
|
21
|
+
* before them. So code — never the model — adds the links to that layout:
|
|
22
|
+
* Account (and Cart, where the header has no cart) to the header's links,
|
|
23
|
+
* and the account and policies to the footer's list of links, each a copy of
|
|
24
|
+
* a link already in that list so it looks like its neighbours.
|
|
25
|
+
*
|
|
26
|
+
* Only where the layout has a list to add to: a header (an App Bar, or an
|
|
27
|
+
* element that renders `header`) holding links, or a Toolbar holding one; a
|
|
28
|
+
* footer (an element that renders `footer`) holding a list of two or more
|
|
29
|
+
* links. A layout with neither is left as it is, and the build says what to
|
|
30
|
+
* add by hand. A link already there — by its address, or to the page that
|
|
31
|
+
* holds that address — is never added twice.
|
|
32
|
+
*
|
|
33
|
+
* Pure: nodes in, nodes out.
|
|
34
|
+
*/ /** One link a store adds to a layout. */ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
35
|
+
/** The commerce plugin's Cart element, which a header may already place (`AI_LAYOUT_CART_ELEMENT`). */ const CART_ELEMENT = 'cart';
|
|
36
|
+
const LINK = 'muiScreenLink';
|
|
37
|
+
const renders = (node, element)=>{
|
|
38
|
+
var _node_props, _node_props1;
|
|
39
|
+
return ((_node_props = node.props) == null ? void 0 : _node_props['element']) === element || ((_node_props1 = node.props) == null ? void 0 : _node_props1['component']) === element || node.componentId === element;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The layout's nodes with a store's links added (AGL-3676), and whether it
|
|
43
|
+
* had a header list and a footer list to add them to. `screenPaths` names
|
|
44
|
+
* the address of each of the site's pages, so a link to a page counts as a
|
|
45
|
+
* link to its address.
|
|
46
|
+
*/ export function aiLayoutWithStoreLinks(nodes, links, screenPaths = {}) {
|
|
47
|
+
var _map_filter_sort_;
|
|
48
|
+
const map = {};
|
|
49
|
+
for (const [id, raw] of Object.entries(nodes)){
|
|
50
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
51
|
+
map[id] = _extends({}, raw, raw.props ? {
|
|
52
|
+
props: _extends({}, raw.props)
|
|
53
|
+
} : {}, raw.nodes ? {
|
|
54
|
+
nodes: [
|
|
55
|
+
...raw.nodes
|
|
56
|
+
]
|
|
57
|
+
} : {});
|
|
58
|
+
}
|
|
59
|
+
const all = Object.values(map);
|
|
60
|
+
const under = (roots)=>{
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const queue = roots.map((root)=>root.$id);
|
|
63
|
+
while(queue.length){
|
|
64
|
+
var _map_id_nodes;
|
|
65
|
+
const id = queue.shift();
|
|
66
|
+
if (seen.has(id) || !map[id]) continue;
|
|
67
|
+
seen.add(id);
|
|
68
|
+
queue.push(...(_map_id_nodes = map[id].nodes) != null ? _map_id_nodes : []);
|
|
69
|
+
}
|
|
70
|
+
return seen;
|
|
71
|
+
};
|
|
72
|
+
const linkChildren = (node)=>{
|
|
73
|
+
var _node_nodes;
|
|
74
|
+
return ((_node_nodes = node.nodes) != null ? _node_nodes : []).filter((id)=>{
|
|
75
|
+
var _map_id;
|
|
76
|
+
return ((_map_id = map[id]) == null ? void 0 : _map_id.componentId) === LINK;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const pathOf = (node)=>{
|
|
80
|
+
var _node_props, _node_props1;
|
|
81
|
+
const href = (_node_props = node.props) == null ? void 0 : _node_props['href'];
|
|
82
|
+
if (typeof href === 'string' && href) return href.replace(/\/+$/, '') || '/';
|
|
83
|
+
const screenId = (_node_props1 = node.props) == null ? void 0 : _node_props1['screenId'];
|
|
84
|
+
const path = typeof screenId === 'string' ? screenPaths[screenId] : undefined;
|
|
85
|
+
return path ? `/${path.replace(/^\/+/, '').replace(/\/+$/, '')}` : null;
|
|
86
|
+
};
|
|
87
|
+
const pathsIn = (ids)=>new Set([
|
|
88
|
+
...ids
|
|
89
|
+
].map((id)=>map[id]).filter((node)=>(node == null ? void 0 : node.componentId) === LINK).map(pathOf).filter(Boolean));
|
|
90
|
+
// The header: its lists of links (the bar's and the phone menu's), else a Toolbar holding one.
|
|
91
|
+
const headerRoots = all.filter((node)=>node.componentId === 'muiAppBar' || renders(node, 'header'));
|
|
92
|
+
const headerIds = under(headerRoots);
|
|
93
|
+
let headerLists = [
|
|
94
|
+
...headerIds
|
|
95
|
+
].map((id)=>map[id]).filter((node)=>linkChildren(node).length >= 2);
|
|
96
|
+
if (!headerLists.length) {
|
|
97
|
+
const toolbar = all.find((node)=>node.componentId === 'muiToolbar' && linkChildren(node).length >= 1);
|
|
98
|
+
headerLists = toolbar ? [
|
|
99
|
+
toolbar
|
|
100
|
+
] : [];
|
|
101
|
+
}
|
|
102
|
+
// The footer: its longest list of links.
|
|
103
|
+
const footerIds = under(all.filter((node)=>renders(node, 'footer')));
|
|
104
|
+
const footerList = (_map_filter_sort_ = [
|
|
105
|
+
...footerIds
|
|
106
|
+
].map((id)=>map[id]).filter((node)=>linkChildren(node).length >= 2).sort((a, b)=>linkChildren(b).length - linkChildren(a).length)[0]) != null ? _map_filter_sort_ : null;
|
|
107
|
+
const added = [];
|
|
108
|
+
const append = (list, wanted, present, region)=>{
|
|
109
|
+
var _list_nodes;
|
|
110
|
+
const children = (_list_nodes = list.nodes) != null ? _list_nodes : [];
|
|
111
|
+
const siblings = linkChildren(list);
|
|
112
|
+
const template = map[siblings[siblings.length - 1]];
|
|
113
|
+
const lastLink = children.reduce((at, id, index)=>{
|
|
114
|
+
var _map_id;
|
|
115
|
+
return ((_map_id = map[id]) == null ? void 0 : _map_id.componentId) === LINK ? index : at;
|
|
116
|
+
}, -1);
|
|
117
|
+
const ids = [];
|
|
118
|
+
for (const link of wanted){
|
|
119
|
+
var _ref, _ref1;
|
|
120
|
+
if (present.has(link.href)) continue;
|
|
121
|
+
const id = `ai_store_${region}_${link.href.replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '')}`;
|
|
122
|
+
if (map[id]) continue;
|
|
123
|
+
const props = _extends({}, (_ref = template == null ? void 0 : template.props) != null ? _ref : {}, {
|
|
124
|
+
children: link.label,
|
|
125
|
+
href: link.href
|
|
126
|
+
});
|
|
127
|
+
delete props['screenId'];
|
|
128
|
+
map[id] = _extends({}, template != null ? template : {}, {
|
|
129
|
+
$id: id,
|
|
130
|
+
componentId: LINK,
|
|
131
|
+
pluginId: (_ref1 = template == null ? void 0 : template.pluginId) != null ? _ref1 : 'mui',
|
|
132
|
+
parentId: list.$id,
|
|
133
|
+
props,
|
|
134
|
+
nodes: []
|
|
135
|
+
});
|
|
136
|
+
ids.push(id);
|
|
137
|
+
if (!added.includes(link.href)) added.push(link.href);
|
|
138
|
+
}
|
|
139
|
+
if (ids.length) list.nodes = [
|
|
140
|
+
...children.slice(0, lastLink + 1),
|
|
141
|
+
...ids,
|
|
142
|
+
...children.slice(lastLink + 1)
|
|
143
|
+
];
|
|
144
|
+
};
|
|
145
|
+
if (headerLists.length) {
|
|
146
|
+
const hasCart = all.some((node)=>node.componentId === CART_ELEMENT);
|
|
147
|
+
const wanted = [
|
|
148
|
+
...links.header,
|
|
149
|
+
...links.cart && !hasCart ? [
|
|
150
|
+
links.cart
|
|
151
|
+
] : []
|
|
152
|
+
];
|
|
153
|
+
const present = pathsIn(headerIds.size ? headerIds : under(headerLists));
|
|
154
|
+
headerLists.forEach((list, index)=>append(list, wanted, present, `header${index}`));
|
|
155
|
+
}
|
|
156
|
+
if (footerList) append(footerList, links.footer, pathsIn(footerIds), 'footer');
|
|
157
|
+
return {
|
|
158
|
+
nodes: added.length ? map : null,
|
|
159
|
+
header: headerLists.length > 0,
|
|
160
|
+
footer: Boolean(footerList),
|
|
161
|
+
added
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//# sourceMappingURL=ai-layout-store-links.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-layout-store-links.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\n/**\n * A STORE'S LINKS IN A LAYOUT THE SITE ALREADY HAS (AGL-3676).\n *\n * A build that makes a site a store and builds no layout of its own adds its\n * account, cart and policy pages to a site whose header and footer were made\n * before them. So code — never the model — adds the links to that layout:\n * Account (and Cart, where the header has no cart) to the header's links,\n * and the account and policies to the footer's list of links, each a copy of\n * a link already in that list so it looks like its neighbours.\n *\n * Only where the layout has a list to add to: a header (an App Bar, or an\n * element that renders `header`) holding links, or a Toolbar holding one; a\n * footer (an element that renders `footer`) holding a list of two or more\n * links. A layout with neither is left as it is, and the build says what to\n * add by hand. A link already there — by its address, or to the page that\n * holds that address — is never added twice.\n *\n * Pure: nodes in, nodes out.\n */\n\n/** One link a store adds to a layout. */\nexport interface AiStoreLayoutLink {\n label: string\n href: string\n}\n\n/** What a store adds to a layout: the header's links (Account, Cart) and the footer's (account, policies). */\nexport interface AiStoreLayoutLinks {\n header: AiStoreLayoutLink[]\n /** The cart's page, linked from the header only where the layout has no cart of its own. */\n cart: AiStoreLayoutLink | null\n footer: AiStoreLayoutLink[]\n}\n\n/** What adding a store's links to a layout came to. */\nexport interface AiLayoutStoreLinksResult {\n /** The layout's nodes with the links added, or `null` where nothing was added. */\n nodes: Record<string, unknown> | null\n /** Whether the layout has a header list and a footer list to add to. */\n header: boolean\n footer: boolean\n /** The addresses added, in order. */\n added: string[]\n}\n\n/** The commerce plugin's Cart element, which a header may already place (`AI_LAYOUT_CART_ELEMENT`). */\nconst CART_ELEMENT = 'cart'\nconst LINK = 'muiScreenLink'\n\ninterface Node {\n $id: string\n componentId?: string\n pluginId?: string\n parentId?: string | null\n nodes?: string[]\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n [key: string]: unknown\n}\n\nconst renders = (node: Node, element: 'header' | 'footer') =>\n node.props?.['element'] === element || node.props?.['component'] === element || node.componentId === element\n\n/**\n * The layout's nodes with a store's links added (AGL-3676), and whether it\n * had a header list and a footer list to add them to. `screenPaths` names\n * the address of each of the site's pages, so a link to a page counts as a\n * link to its address.\n */\nexport function aiLayoutWithStoreLinks(\n nodes: Readonly<Record<string, unknown>>,\n links: AiStoreLayoutLinks,\n screenPaths: Readonly<Record<string, string>> = {},\n): AiLayoutStoreLinksResult {\n const map: Record<string, Node> = {}\n for (const [id, raw] of Object.entries(nodes as Record<string, Node>)) {\n if (!raw || typeof raw !== 'object') continue\n map[id] = { ...raw, ...(raw.props ? { props: { ...raw.props } } : {}), ...(raw.nodes ? { nodes: [...raw.nodes] } : {}) }\n }\n const all = Object.values(map)\n const under = (roots: readonly Node[]): Set<string> => {\n const seen = new Set<string>()\n const queue = roots.map((root) => root.$id)\n while (queue.length) {\n const id = queue.shift() as string\n if (seen.has(id) || !map[id]) continue\n seen.add(id)\n queue.push(...(map[id].nodes ?? []))\n }\n return seen\n }\n const linkChildren = (node: Node) => (node.nodes ?? []).filter((id) => map[id]?.componentId === LINK)\n const pathOf = (node: Node): string | null => {\n const href = node.props?.['href']\n if (typeof href === 'string' && href) return href.replace(/\\/+$/, '') || '/'\n const screenId = node.props?.['screenId']\n const path = typeof screenId === 'string' ? screenPaths[screenId] : undefined\n return path ? `/${path.replace(/^\\/+/, '').replace(/\\/+$/, '')}` : null\n }\n const pathsIn = (ids: Set<string>) =>\n new Set([...ids].map((id) => map[id]).filter((node) => node?.componentId === LINK).map(pathOf).filter(Boolean))\n\n // The header: its lists of links (the bar's and the phone menu's), else a Toolbar holding one.\n const headerRoots = all.filter((node) => node.componentId === 'muiAppBar' || renders(node, 'header'))\n const headerIds = under(headerRoots)\n let headerLists = [...headerIds].map((id) => map[id]).filter((node) => linkChildren(node).length >= 2)\n if (!headerLists.length) {\n const toolbar = all.find((node) => node.componentId === 'muiToolbar' && linkChildren(node).length >= 1)\n headerLists = toolbar ? [toolbar] : []\n }\n // The footer: its longest list of links.\n const footerIds = under(all.filter((node) => renders(node, 'footer')))\n const footerList =\n [...footerIds]\n .map((id) => map[id])\n .filter((node) => linkChildren(node).length >= 2)\n .sort((a, b) => linkChildren(b).length - linkChildren(a).length)[0] ?? null\n\n const added: string[] = []\n const append = (list: Node, wanted: readonly AiStoreLayoutLink[], present: Set<unknown>, region: string) => {\n const children = list.nodes ?? []\n const siblings = linkChildren(list)\n const template = map[siblings[siblings.length - 1]]\n const lastLink = children.reduce((at, id, index) => (map[id]?.componentId === LINK ? index : at), -1)\n const ids: string[] = []\n for (const link of wanted) {\n if (present.has(link.href)) continue\n const id = `ai_store_${region}_${link.href.replace(/[^a-z0-9]+/gi, '_').replace(/^_+|_+$/g, '')}`\n if (map[id]) continue\n const props = { ...(template?.props ?? {}), children: link.label, href: link.href }\n delete props['screenId']\n map[id] = {\n ...(template ?? {}),\n $id: id,\n componentId: LINK,\n pluginId: template?.pluginId ?? 'mui',\n parentId: list.$id,\n props,\n nodes: [],\n }\n ids.push(id)\n if (!added.includes(link.href)) added.push(link.href)\n }\n if (ids.length) list.nodes = [...children.slice(0, lastLink + 1), ...ids, ...children.slice(lastLink + 1)]\n }\n\n if (headerLists.length) {\n const hasCart = all.some((node) => node.componentId === CART_ELEMENT)\n const wanted = [...links.header, ...(links.cart && !hasCart ? [links.cart] : [])]\n const present = pathsIn(headerIds.size ? headerIds : under(headerLists))\n headerLists.forEach((list, index) => append(list, wanted, present, `header${index}`))\n }\n if (footerList) append(footerList, links.footer, pathsIn(footerIds), 'footer')\n return {\n nodes: added.length ? (map as Record<string, unknown>) : null,\n header: headerLists.length > 0,\n footer: Boolean(footerList),\n added,\n }\n}\n"],"names":["CART_ELEMENT","LINK","renders","node","element","props","componentId","aiLayoutWithStoreLinks","nodes","links","screenPaths","map","id","raw","Object","entries","all","values","under","roots","seen","Set","queue","root","$id","length","shift","has","add","push","linkChildren","filter","pathOf","href","replace","screenId","path","undefined","pathsIn","ids","Boolean","headerRoots","headerIds","headerLists","toolbar","find","footerIds","footerList","sort","a","b","added","append","list","wanted","present","region","children","siblings","template","lastLink","reduce","at","index","link","label","pluginId","parentId","includes","slice","hasCart","some","header","cart","size","forEach","footer"],"mappings":"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"}
|
|
@@ -100,6 +100,11 @@ export interface AiInventoryDataset {
|
|
|
100
100
|
* `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.
|
|
101
101
|
*/
|
|
102
102
|
fieldIds?: string[];
|
|
103
|
+
/**
|
|
104
|
+
* The id of the field holding each record's photo, where it has one
|
|
105
|
+
* (AGL-3616): what a record template's Image binds as its `src`.
|
|
106
|
+
*/
|
|
107
|
+
imageField?: string;
|
|
103
108
|
}
|
|
104
109
|
export interface AiInventoryCollection {
|
|
105
110
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.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\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n /**\n * The platform's starter home page, untouched since the site was created\n * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering\n * `/`, with no version but the one it was provisioned with. A plan may put\n * its own home page at its address, and publishing that page retires this\n * one (AGL-3408). Absent on every other screen, and on a starter home the\n * owner has edited, which a plan keeps and plans around.\n */\n replaceable?: true\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}${\n screen.replaceable ? ' \\u00b7 starter home, a planned home page at / replaces it' : ''\n }`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n const homes = (inventory?.screens ?? []).filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n // A starter home a planned home page replaces (AGL-3594) is retired when\n // that page is published, so a link is sent to the page that replaces it\n // wherever the site already has one.\n const kept = homes.filter((screen) => !screen.replaceable)\n return (kept.length ? kept : homes).map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","replaceable","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","homes","filter","has","trim","replace","test","kept","length","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-site-inventory.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\n/**\n * The site's inventory as a model sees it (AGL-2935): what the site already\n * has, so a plan reuses before it creates (doctrine rule 7).\n *\n * COMPACT by construction (AGL-2937): ids, names, prop shapes and slugs —\n * never a node tree, never a record's content. A component is its name and\n * the props an instance fills in; a form is its field names; a dataset its\n * field names. That is everything a planner needs to decide \"reuse this\"\n * and a generator needs to place a reference, and it is a few hundred\n * tokens where the trees would be tens of thousands.\n *\n * CAPPED three times, and the three are different numbers on purpose\n * (AGL-2937). The reader holds each kind to `AI_SITE_INVENTORY_MAX_PER_KIND`;\n * the prompt block LISTS only `AI_SITE_INVENTORY_LISTED_PER_KIND` of them and\n * is held to `AI_SITE_INVENTORY_MAX_CHARS` besides; and the rest are answered\n * on request, through the lookup tool the generation loop offers beside every\n * door's own (`tools/ai-inventory-lookup-tool.ts`).\n *\n * The listing cap is what a prompt can afford: the block is volatile, so it\n * is billed at full input rate on every request and every re-ask, and a site\n * with two hundred components would spend most of a plan's prompt on rows it\n * will not use. The read cap is what a LOOKUP can answer from, which is a\n * different question — a search for \"pricing card\" is worth nothing if the\n * card was never read. A kind cut by either is named in `truncated`, so the\n * model is told more exist rather than concluding they do not.\n *\n * The shapes live here, apart from the reader (`runtime/site-inventory.ts`),\n * so the prompt block, the validators and their specs can hold an inventory\n * without loading the Admin SDK the reader needs.\n */\n\n/**\n * Rows per kind the reader holds. Above the listing cap on purpose: the rows\n * past it are what the lookup tool searches, and a row never read cannot be\n * found. One projection of a few fields each, so the read is small even at\n * this cap.\n */\nexport const AI_SITE_INVENTORY_MAX_PER_KIND = 200\n\n/** Rows per kind the prompt block lists; the rest are found by lookup. */\nexport const AI_SITE_INVENTORY_LISTED_PER_KIND = 40\n\n/** The rendered block's ceiling: 2,000 tokens at four characters a token. */\nexport const AI_SITE_INVENTORY_MAX_CHARS = 8_000\n\nexport type AiInventoryKind =\n | 'components'\n | 'layouts'\n | 'templates'\n | 'forms'\n | 'datasets'\n | 'collections'\n | 'screens'\n\nexport const AI_INVENTORY_KINDS: readonly AiInventoryKind[] = [\n 'components',\n 'layouts',\n 'templates',\n 'forms',\n 'datasets',\n 'collections',\n 'screens',\n]\n\n/**\n * A reusable component's declared props as an instance may fill them: prop\n * name → its declared type (`text`, `image`, `href`, `number`, `boolean`, …).\n */\nexport type AiComponentPropTypes = Record<string, string>\n\nexport interface AiInventoryComponent {\n id: string\n name: string\n /** The props an instance fills in: name → declared type. */\n props: AiComponentPropTypes\n /**\n * The facts in square brackets each prop's default shows until a page sets\n * its own, by prop name (AGL-3056): what a page placing the component still\n * asks the member to fill. Read for the member's note, never listed in a\n * prompt; absent when no default holds one.\n */\n bracketedDefaults?: Record<string, string[]>\n}\n\nexport interface AiInventoryLayout {\n id: string\n name: string\n /** The layout this one nests inside, when it nests. */\n parentId: string | null\n}\n\nexport interface AiInventoryTemplate {\n id: string\n name: string\n kind: string\n}\n\nexport interface AiInventoryForm {\n id: string\n name: string\n fields: string[]\n}\n\nexport interface AiInventoryDataset {\n id: string\n name: string\n fields: string[]\n /**\n * The same fields' ids, in the same order: what a record template binds as\n * `{{item.<id>}}` (AGL-3475). Absent where the reader did not see them.\n */\n fieldIds?: string[]\n /**\n * The id of the field holding each record's photo, where it has one\n * (AGL-3616): what a record template's Image binds as its `src`.\n */\n imageField?: string\n}\n\nexport interface AiInventoryCollection {\n id: string\n name: string\n slug: string\n}\n\nexport interface AiInventoryScreen {\n id: string\n name: string\n slug: string\n layoutId: string | null\n /** A collection's entry template rather than a page of its own. */\n template: boolean\n /**\n * The platform's starter home page, untouched since the site was created\n * (AGL-3594): the screen `host.defaultHomeScreenId` names, still answering\n * `/`, with no version but the one it was provisioned with. A plan may put\n * its own home page at its address, and publishing that page retires this\n * one (AGL-3408). Absent on every other screen, and on a starter home the\n * owner has edited, which a plan keeps and plans around.\n */\n replaceable?: true\n}\n\n/**\n * The site's one theme, as a planner and the email rule read it: a short\n * description, the light scheme's colors by palette path, and the families\n * the theme loads.\n */\nexport interface AiInventoryTheme {\n summary: string[]\n colors: Record<string, string>\n fonts: string[]\n}\n\nexport interface AiSiteInventory {\n hostId: string\n components: AiInventoryComponent[]\n layouts: AiInventoryLayout[]\n templates: AiInventoryTemplate[]\n forms: AiInventoryForm[]\n datasets: AiInventoryDataset[]\n collections: AiInventoryCollection[]\n screens: AiInventoryScreen[]\n theme: AiInventoryTheme | null\n /** Kinds with more records than are listed. */\n truncated: AiInventoryKind[]\n}\n\n/** One inventory record, whatever kind it is. */\nexport type AiInventoryRow =\n | AiInventoryComponent\n | AiInventoryLayout\n | AiInventoryTemplate\n | AiInventoryForm\n | AiInventoryDataset\n | AiInventoryCollection\n | AiInventoryScreen\n\n/**\n * The rows of one kind. The inventory's fields are named after their kinds,\n * so this is a lookup rather than a switch, and it is the one place that\n * relies on the correspondence: the prompt block, the lookup tool and their\n * specs all read a kind through it.\n */\nexport function aiInventoryRows(\n inventory: AiSiteInventory,\n kind: AiInventoryKind,\n): readonly AiInventoryRow[] {\n return inventory[kind]\n}\n\n/** The heading one kind's lines are listed under, naming its columns in order. */\nexport const AI_INVENTORY_KIND_HEADINGS: Readonly<Record<AiInventoryKind, string>> = {\n components: 'Reusable components (id \\u00b7 name \\u00b7 props an instance fills)',\n layouts: 'Layouts (id \\u00b7 name)',\n templates: 'Templates (id \\u00b7 name \\u00b7 kind)',\n forms: 'Forms (id \\u00b7 name \\u00b7 fields)',\n datasets: 'Datasets (id \\u00b7 name \\u00b7 fields)',\n collections: 'Content collections (id \\u00b7 name \\u00b7 slug)',\n screens: 'Pages (id \\u00b7 name \\u00b7 slug)',\n}\n\n/**\n * One record as a line, in the order its heading names the columns. The one\n * renderer: the prompt block and the lookup tool's answer read alike, so a\n * row found by lookup is the same shape as a row that was listed.\n */\nexport function aiInventoryLine(kind: AiInventoryKind, row: AiInventoryRow): string {\n const head = `${row.id} \\u00b7 ${row.name}`\n switch (kind) {\n case 'components': {\n const props = Object.entries((row as AiInventoryComponent).props)\n .map(([name, type]) => `${name}:${type}`)\n .join(', ')\n return `${head} \\u00b7 ${props || 'no props'}`\n }\n case 'layouts':\n return head\n case 'templates':\n return `${head} \\u00b7 ${(row as AiInventoryTemplate).kind}`\n case 'forms':\n return `${head} \\u00b7 ${(row as AiInventoryForm).fields.join(', ')}`\n case 'datasets':\n return `${head} \\u00b7 ${(row as AiInventoryDataset).fields.join(', ')}`\n case 'collections':\n return `${head} \\u00b7 ${(row as AiInventoryCollection).slug}`\n case 'screens': {\n const screen = row as AiInventoryScreen\n return `${head} \\u00b7 ${screen.slug}${screen.template ? ' \\u00b7 entry template' : ''}${\n screen.replaceable ? ' \\u00b7 starter home, a planned home page at / replaces it' : ''\n }`\n }\n }\n}\n\n/** Every word of a record a lookup searches: its line, which holds them all. */\nexport function aiInventoryHaystack(kind: AiInventoryKind, row: AiInventoryRow): string {\n return aiInventoryLine(kind, row).toLowerCase()\n}\n\n/** The addresses a site's home screen is stored under. */\nconst HOME_SLUGS = new Set(['', 'home', 'index'])\n\n/**\n * The site's home screens, by id (AGL-3056): a screen at the root or at\n * `home`, as a seeded site stores its first screen, or one named Home. A link\n * with nowhere to go is sent there, which is why the doctrine names them.\n */\nexport function aiHomeScreenIds(inventory: AiSiteInventory | null): string[] {\n const homes = (inventory?.screens ?? []).filter(\n (screen) =>\n !screen.template &&\n (HOME_SLUGS.has(screen.slug.trim().replace(/^\\/+|\\/+$/g, '').toLowerCase()) ||\n /^home(?:\\s?page)?$/i.test(screen.name.trim())),\n )\n // A starter home a planned home page replaces (AGL-3594) is retired when\n // that page is published, so a link is sent to the page that replaces it\n // wherever the site already has one.\n const kept = homes.filter((screen) => !screen.replaceable)\n return (kept.length ? kept : homes).map((screen) => screen.id)\n}\n\n/** A site the org does not own; the inventory is never read for it. */\nexport class AiInventoryScopeError extends Error {\n constructor(hostId: string) {\n super(`host ${hostId} is not a site of the workspace that asked`)\n this.name = 'AiInventoryScopeError'\n }\n}\n\nexport function emptyAiSiteInventory(hostId: string): AiSiteInventory {\n return {\n hostId,\n components: [],\n layouts: [],\n templates: [],\n forms: [],\n datasets: [],\n collections: [],\n screens: [],\n theme: null,\n truncated: [],\n }\n}\n"],"names":["AI_SITE_INVENTORY_MAX_PER_KIND","AI_SITE_INVENTORY_LISTED_PER_KIND","AI_SITE_INVENTORY_MAX_CHARS","AI_INVENTORY_KINDS","aiInventoryRows","inventory","kind","AI_INVENTORY_KIND_HEADINGS","components","layouts","templates","forms","datasets","collections","screens","aiInventoryLine","row","head","id","name","props","Object","entries","map","type","join","fields","slug","screen","template","replaceable","aiInventoryHaystack","toLowerCase","HOME_SLUGS","Set","aiHomeScreenIds","homes","filter","has","trim","replace","test","kept","length","AiInventoryScopeError","Error","hostId","emptyAiSiteInventory","theme","truncated"],"mappings":"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"}
|