@aglyn/plugins-ai 1.0.0-beta.249 → 1.0.0-beta.252
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-brief-dialog.component.js +15 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +1 -1
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +20 -2
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +11 -0
- package/src/lib/components/ai-jobs-store.js +22 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +3 -2
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.js +7 -4
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
- package/src/lib/jobs/ai-job-drafts.js +46 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +6 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
- package/src/lib/jobs/ai-job-form-reuse.js +136 -0
- package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-form-step.js +63 -11
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +6 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +12 -1
- package/src/lib/jobs/ai-job-page-language.js +48 -11
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +8 -3
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-plan-step.js +39 -1
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +5 -3
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
- package/src/lib/jobs/ai-job-site-content.js +7 -5
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +17 -0
- package/src/lib/jobs/ai-job-site-datasets.js +24 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +5 -2
- package/src/lib/jobs/ai-job-site-step.js +20 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.js +6 -3
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +22 -8
- package/src/lib/jobs/ai-layout-stock-photos.js +39 -8
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
- package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
- package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
- package/src/lib/layout-language/ai-layout-gaps.d.ts +15 -0
- package/src/lib/layout-language/ai-layout-gaps.js +57 -7
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -3
- package/src/lib/model/ai-build-plan.js +11 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +32 -18
- package/src/lib/model/ai-credit-estimate.js +36 -18
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +30 -0
- package/src/lib/model/ai-job-activity.js +35 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +5 -3
- package/src/lib/model/ai-media-credits.js +7 -5
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-site-job.js +6 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +5 -0
- package/src/lib/model/ai-site-store-pages.js +2 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/plugin-config.js +2 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
- package/src/lib/providers/catalog.d.ts +24 -17
- package/src/lib/providers/catalog.js +63 -30
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.d.ts +1 -1
- package/src/lib/providers/model-choice.js +39 -30
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.d.ts +20 -1
- package/src/lib/providers/routing.js +47 -8
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +37 -1
- package/src/lib/runtime/ai-dev-env.js +59 -6
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +8 -0
- package/src/lib/runtime/ai-eval-live.js +34 -8
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +4 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +17 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/seo-fields.js +4 -3
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-assist.js +3 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/ai-media-svg.js +4 -0
- package/src/lib/server/ai-media-svg.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +2 -5
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
* THE ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).
|
|
19
|
+
*
|
|
20
|
+
* The stock photo libraries are tagged in English, so a site in another
|
|
21
|
+
* language is searched by an English subject (`aiStockSourceTerms`). The
|
|
22
|
+
* business type the person typed is the surest word for what the site is
|
|
23
|
+
* about, once it is English: "Una panadería familiar … con pan dulce" read
|
|
24
|
+
* as an English noun phrase searched "con pan dulce" and placed the Río
|
|
25
|
+
* Dulce, the kind of site picked ("restaurant cafe") placed a coffee mug as a
|
|
26
|
+
* bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's
|
|
27
|
+
* photo words for a florist, "wedding bouquets event flowers", led with
|
|
28
|
+
* "wedding". So a business this table names leads the search, ahead of the
|
|
29
|
+
* plan's photo words, which decide only for a business it does not name.
|
|
30
|
+
*
|
|
31
|
+
* This table names the common kinds of local business in Spanish, French,
|
|
32
|
+
* Portuguese, German and Italian, each with the English words a library is
|
|
33
|
+
* searched by. It is deliberately small: a kind belongs here when people
|
|
34
|
+
* start sites for it and its name is not already English. Words English
|
|
35
|
+
* shares ("café", "restaurant", "boutique", "pizzeria") are left out, because
|
|
36
|
+
* the guided start's own brief is English around the person's words ("Make
|
|
37
|
+
* it a restaurant & café site") and must never decide the subject; such a
|
|
38
|
+
* site falls through to the kind it picked, as before.
|
|
39
|
+
*
|
|
40
|
+
* A word matches whole, accents and case aside: "panaderia" and "Panadería"
|
|
41
|
+
* meet, so a name two languages spell alike ("farmacia", "fotógrafo") is
|
|
42
|
+
* listed once; "pan" never matches inside "panadería", and a German compound
|
|
43
|
+
* ("Familienbäckerei") matches only if it is listed itself. The first listed
|
|
44
|
+
* name the text says wins, the longer of two that start at the same word —
|
|
45
|
+
* "peluquería canina" is a dog groomer, "peluquería" a hair salon — so the
|
|
46
|
+
* head noun a Romance-language business type opens with decides.
|
|
47
|
+
*/
|
|
48
|
+
/** A kind of business: the English words its photos are searched by, and its names in other languages. */
|
|
49
|
+
interface AiStockBusinessNoun {
|
|
50
|
+
/** What a stock photo library is searched by: "bakery". */
|
|
51
|
+
english: string;
|
|
52
|
+
/** Its names in es, fr, pt, de and it, as people write them. */
|
|
53
|
+
names: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
export declare const AI_STOCK_BUSINESS_NOUNS: readonly AiStockBusinessNoun[];
|
|
56
|
+
/**
|
|
57
|
+
* The English words for the kind of business a text in another language
|
|
58
|
+
* names ({@link AI_STOCK_BUSINESS_NOUNS}): "bakery" for "Una panadería
|
|
59
|
+
* familiar de San Antonio con pan dulce". Empty when it names none listed.
|
|
60
|
+
*/
|
|
61
|
+
export declare function aiStockBusinessNoun(text: string): string;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
* THE ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).
|
|
18
|
+
*
|
|
19
|
+
* The stock photo libraries are tagged in English, so a site in another
|
|
20
|
+
* language is searched by an English subject (`aiStockSourceTerms`). The
|
|
21
|
+
* business type the person typed is the surest word for what the site is
|
|
22
|
+
* about, once it is English: "Una panadería familiar … con pan dulce" read
|
|
23
|
+
* as an English noun phrase searched "con pan dulce" and placed the Río
|
|
24
|
+
* Dulce, the kind of site picked ("restaurant cafe") placed a coffee mug as a
|
|
25
|
+
* bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's
|
|
26
|
+
* photo words for a florist, "wedding bouquets event flowers", led with
|
|
27
|
+
* "wedding". So a business this table names leads the search, ahead of the
|
|
28
|
+
* plan's photo words, which decide only for a business it does not name.
|
|
29
|
+
*
|
|
30
|
+
* This table names the common kinds of local business in Spanish, French,
|
|
31
|
+
* Portuguese, German and Italian, each with the English words a library is
|
|
32
|
+
* searched by. It is deliberately small: a kind belongs here when people
|
|
33
|
+
* start sites for it and its name is not already English. Words English
|
|
34
|
+
* shares ("café", "restaurant", "boutique", "pizzeria") are left out, because
|
|
35
|
+
* the guided start's own brief is English around the person's words ("Make
|
|
36
|
+
* it a restaurant & café site") and must never decide the subject; such a
|
|
37
|
+
* site falls through to the kind it picked, as before.
|
|
38
|
+
*
|
|
39
|
+
* A word matches whole, accents and case aside: "panaderia" and "Panadería"
|
|
40
|
+
* meet, so a name two languages spell alike ("farmacia", "fotógrafo") is
|
|
41
|
+
* listed once; "pan" never matches inside "panadería", and a German compound
|
|
42
|
+
* ("Familienbäckerei") matches only if it is listed itself. The first listed
|
|
43
|
+
* name the text says wins, the longer of two that start at the same word —
|
|
44
|
+
* "peluquería canina" is a dog groomer, "peluquería" a hair salon — so the
|
|
45
|
+
* head noun a Romance-language business type opens with decides.
|
|
46
|
+
*/ /** A kind of business: the English words its photos are searched by, and its names in other languages. */ export const AI_STOCK_BUSINESS_NOUNS = [
|
|
47
|
+
{
|
|
48
|
+
english: 'bakery',
|
|
49
|
+
names: [
|
|
50
|
+
'panadería',
|
|
51
|
+
'boulangerie',
|
|
52
|
+
'padaria',
|
|
53
|
+
'Bäckerei',
|
|
54
|
+
'panetteria',
|
|
55
|
+
'panificio'
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
english: 'pastry shop',
|
|
60
|
+
names: [
|
|
61
|
+
'pastelería',
|
|
62
|
+
'repostería',
|
|
63
|
+
'pâtisserie',
|
|
64
|
+
'confeitaria',
|
|
65
|
+
'Konditorei',
|
|
66
|
+
'pasticceria'
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
english: 'tacos',
|
|
71
|
+
names: [
|
|
72
|
+
'taquería'
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
english: 'cafe',
|
|
77
|
+
names: [
|
|
78
|
+
'cafetería',
|
|
79
|
+
'caffetteria'
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
english: 'restaurant',
|
|
84
|
+
names: [
|
|
85
|
+
'restaurante',
|
|
86
|
+
'ristorante',
|
|
87
|
+
'Gaststätte'
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
// A library knows the trattoria by its own name, a narrower search than "restaurant".
|
|
91
|
+
{
|
|
92
|
+
english: 'trattoria',
|
|
93
|
+
names: [
|
|
94
|
+
'trattoria',
|
|
95
|
+
'osteria'
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
english: 'butcher shop',
|
|
100
|
+
names: [
|
|
101
|
+
'carnicería',
|
|
102
|
+
'boucherie',
|
|
103
|
+
'açougue',
|
|
104
|
+
'Metzgerei',
|
|
105
|
+
'macelleria'
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
english: 'florist',
|
|
110
|
+
names: [
|
|
111
|
+
'floristería',
|
|
112
|
+
'florería',
|
|
113
|
+
'fleuriste',
|
|
114
|
+
'floricultura',
|
|
115
|
+
'Blumenladen',
|
|
116
|
+
'fioraio',
|
|
117
|
+
'fioreria'
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
english: 'hair salon',
|
|
122
|
+
names: [
|
|
123
|
+
'peluquería',
|
|
124
|
+
'salon de coiffure',
|
|
125
|
+
'coiffeur',
|
|
126
|
+
'coiffeuse',
|
|
127
|
+
'cabeleireiro',
|
|
128
|
+
'Friseur',
|
|
129
|
+
'Friseursalon',
|
|
130
|
+
'parrucchiere'
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
english: 'dog grooming',
|
|
135
|
+
names: [
|
|
136
|
+
'peluquería canina',
|
|
137
|
+
'toilettage',
|
|
138
|
+
'banho e tosa',
|
|
139
|
+
'Hundesalon',
|
|
140
|
+
'toelettatura'
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
english: 'beauty salon',
|
|
145
|
+
names: [
|
|
146
|
+
'salón de belleza',
|
|
147
|
+
'institut de beauté',
|
|
148
|
+
'salão de beleza',
|
|
149
|
+
'Kosmetikstudio',
|
|
150
|
+
'centro estetico'
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
english: 'barber shop',
|
|
155
|
+
names: [
|
|
156
|
+
'barbería',
|
|
157
|
+
'barbearia',
|
|
158
|
+
'Barbier',
|
|
159
|
+
'barbiere'
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
english: 'auto repair',
|
|
164
|
+
names: [
|
|
165
|
+
'taller mecánico',
|
|
166
|
+
'oficina mecânica',
|
|
167
|
+
'Autowerkstatt',
|
|
168
|
+
'Kfz-Werkstatt',
|
|
169
|
+
'officina meccanica',
|
|
170
|
+
'autofficina'
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
english: 'dentist',
|
|
175
|
+
names: [
|
|
176
|
+
'dentista',
|
|
177
|
+
'clínica dental',
|
|
178
|
+
'dentiste',
|
|
179
|
+
'cabinet dentaire',
|
|
180
|
+
'Zahnarzt',
|
|
181
|
+
'Zahnarztpraxis',
|
|
182
|
+
'studio dentistico'
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
english: 'veterinary clinic',
|
|
187
|
+
names: [
|
|
188
|
+
'veterinaria',
|
|
189
|
+
'veterinario',
|
|
190
|
+
'vétérinaire',
|
|
191
|
+
'Tierarzt',
|
|
192
|
+
'Tierarztpraxis'
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
english: 'pharmacy',
|
|
197
|
+
names: [
|
|
198
|
+
'farmacia',
|
|
199
|
+
'pharmacie',
|
|
200
|
+
'Apotheke'
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
english: 'bookstore',
|
|
205
|
+
names: [
|
|
206
|
+
'librería',
|
|
207
|
+
'librairie',
|
|
208
|
+
'livraria',
|
|
209
|
+
'Buchhandlung'
|
|
210
|
+
]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
english: 'photographer',
|
|
214
|
+
names: [
|
|
215
|
+
'fotógrafo',
|
|
216
|
+
'fotógrafa',
|
|
217
|
+
'photographe',
|
|
218
|
+
'Fotograf',
|
|
219
|
+
'Fotografin'
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
english: 'plumber',
|
|
224
|
+
names: [
|
|
225
|
+
'fontanero',
|
|
226
|
+
'plomero',
|
|
227
|
+
'plombier',
|
|
228
|
+
'encanador',
|
|
229
|
+
'Klempner',
|
|
230
|
+
'idraulico'
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
english: 'cleaning service',
|
|
235
|
+
names: [
|
|
236
|
+
'servicio de limpieza',
|
|
237
|
+
'nettoyage',
|
|
238
|
+
'limpeza',
|
|
239
|
+
'Reinigung',
|
|
240
|
+
'impresa di pulizie'
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
/** A text's words, lowercase, without accents: "Panadería" and "panaderia" are one word. */ function foldedWords(text) {
|
|
245
|
+
return text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/ß/g, 'ss').split(/[^a-z]+/).filter(Boolean);
|
|
246
|
+
}
|
|
247
|
+
/** Every listed name as its words, the longest first, so the longer of two at one word wins. */ const NAMES = AI_STOCK_BUSINESS_NOUNS.flatMap(({ english, names })=>names.map((name)=>({
|
|
248
|
+
words: foldedWords(name),
|
|
249
|
+
english
|
|
250
|
+
}))).sort((a, b)=>b.words.length - a.words.length);
|
|
251
|
+
/**
|
|
252
|
+
* The English words for the kind of business a text in another language
|
|
253
|
+
* names ({@link AI_STOCK_BUSINESS_NOUNS}): "bakery" for "Una panadería
|
|
254
|
+
* familiar de San Antonio con pan dulce". Empty when it names none listed.
|
|
255
|
+
*/ export function aiStockBusinessNoun(text) {
|
|
256
|
+
const words = foldedWords(text);
|
|
257
|
+
for(let at = 0; at < words.length; at += 1){
|
|
258
|
+
const found = NAMES.find((name)=>name.words.every((word, index)=>words[at + index] === word));
|
|
259
|
+
if (found) return found.english;
|
|
260
|
+
}
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=ai-stock-business-nouns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-stock-business-nouns.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 ENGLISH NAME OF A BUSINESS TYPE WRITTEN IN ANOTHER LANGUAGE (AGL-3779).\n *\n * The stock photo libraries are tagged in English, so a site in another\n * language is searched by an English subject (`aiStockSourceTerms`). The\n * business type the person typed is the surest word for what the site is\n * about, once it is English: \"Una panadería familiar … con pan dulce\" read\n * as an English noun phrase searched \"con pan dulce\" and placed the Río\n * Dulce, the kind of site picked (\"restaurant cafe\") placed a coffee mug as a\n * bakery's hero (jobs ih6kVvCGP1 and iGmqrEgyW4, beta.250), and a live plan's\n * photo words for a florist, \"wedding bouquets event flowers\", led with\n * \"wedding\". So a business this table names leads the search, ahead of the\n * plan's photo words, which decide only for a business it does not name.\n *\n * This table names the common kinds of local business in Spanish, French,\n * Portuguese, German and Italian, each with the English words a library is\n * searched by. It is deliberately small: a kind belongs here when people\n * start sites for it and its name is not already English. Words English\n * shares (\"café\", \"restaurant\", \"boutique\", \"pizzeria\") are left out, because\n * the guided start's own brief is English around the person's words (\"Make\n * it a restaurant & café site\") and must never decide the subject; such a\n * site falls through to the kind it picked, as before.\n *\n * A word matches whole, accents and case aside: \"panaderia\" and \"Panadería\"\n * meet, so a name two languages spell alike (\"farmacia\", \"fotógrafo\") is\n * listed once; \"pan\" never matches inside \"panadería\", and a German compound\n * (\"Familienbäckerei\") matches only if it is listed itself. The first listed\n * name the text says wins, the longer of two that start at the same word —\n * \"peluquería canina\" is a dog groomer, \"peluquería\" a hair salon — so the\n * head noun a Romance-language business type opens with decides.\n */\n\n/** A kind of business: the English words its photos are searched by, and its names in other languages. */\ninterface AiStockBusinessNoun {\n /** What a stock photo library is searched by: \"bakery\". */\n english: string\n /** Its names in es, fr, pt, de and it, as people write them. */\n names: readonly string[]\n}\n\nexport const AI_STOCK_BUSINESS_NOUNS: readonly AiStockBusinessNoun[] = [\n { english: 'bakery', names: ['panadería', 'boulangerie', 'padaria', 'Bäckerei', 'panetteria', 'panificio'] },\n { english: 'pastry shop', names: ['pastelería', 'repostería', 'pâtisserie', 'confeitaria', 'Konditorei', 'pasticceria'] },\n { english: 'tacos', names: ['taquería'] },\n { english: 'cafe', names: ['cafetería', 'caffetteria'] },\n { english: 'restaurant', names: ['restaurante', 'ristorante', 'Gaststätte'] },\n // A library knows the trattoria by its own name, a narrower search than \"restaurant\".\n { english: 'trattoria', names: ['trattoria', 'osteria'] },\n { english: 'butcher shop', names: ['carnicería', 'boucherie', 'açougue', 'Metzgerei', 'macelleria'] },\n { english: 'florist', names: ['floristería', 'florería', 'fleuriste', 'floricultura', 'Blumenladen', 'fioraio', 'fioreria'] },\n {\n english: 'hair salon',\n names: ['peluquería', 'salon de coiffure', 'coiffeur', 'coiffeuse', 'cabeleireiro', 'Friseur', 'Friseursalon', 'parrucchiere'],\n },\n { english: 'dog grooming', names: ['peluquería canina', 'toilettage', 'banho e tosa', 'Hundesalon', 'toelettatura'] },\n { english: 'beauty salon', names: ['salón de belleza', 'institut de beauté', 'salão de beleza', 'Kosmetikstudio', 'centro estetico'] },\n { english: 'barber shop', names: ['barbería', 'barbearia', 'Barbier', 'barbiere'] },\n { english: 'auto repair', names: ['taller mecánico', 'oficina mecânica', 'Autowerkstatt', 'Kfz-Werkstatt', 'officina meccanica', 'autofficina'] },\n { english: 'dentist', names: ['dentista', 'clínica dental', 'dentiste', 'cabinet dentaire', 'Zahnarzt', 'Zahnarztpraxis', 'studio dentistico'] },\n { english: 'veterinary clinic', names: ['veterinaria', 'veterinario', 'vétérinaire', 'Tierarzt', 'Tierarztpraxis'] },\n { english: 'pharmacy', names: ['farmacia', 'pharmacie', 'Apotheke'] },\n { english: 'bookstore', names: ['librería', 'librairie', 'livraria', 'Buchhandlung'] },\n { english: 'photographer', names: ['fotógrafo', 'fotógrafa', 'photographe', 'Fotograf', 'Fotografin'] },\n { english: 'plumber', names: ['fontanero', 'plomero', 'plombier', 'encanador', 'Klempner', 'idraulico'] },\n { english: 'cleaning service', names: ['servicio de limpieza', 'nettoyage', 'limpeza', 'Reinigung', 'impresa di pulizie'] },\n]\n\n/** A text's words, lowercase, without accents: \"Panadería\" and \"panaderia\" are one word. */\nfunction foldedWords(text: string): string[] {\n return text\n .normalize('NFD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/ß/g, 'ss')\n .split(/[^a-z]+/)\n .filter(Boolean)\n}\n\n/** Every listed name as its words, the longest first, so the longer of two at one word wins. */\nconst NAMES: ReadonlyArray<{ words: string[]; english: string }> = AI_STOCK_BUSINESS_NOUNS.flatMap(({ english, names }) =>\n names.map((name) => ({ words: foldedWords(name), english })),\n).sort((a, b) => b.words.length - a.words.length)\n\n/**\n * The English words for the kind of business a text in another language\n * names ({@link AI_STOCK_BUSINESS_NOUNS}): \"bakery\" for \"Una panadería\n * familiar de San Antonio con pan dulce\". Empty when it names none listed.\n */\nexport function aiStockBusinessNoun(text: string): string {\n const words = foldedWords(text)\n for (let at = 0; at < words.length; at += 1) {\n const found = NAMES.find((name) => name.words.every((word, index) => words[at + index] === word))\n if (found) return found.english\n }\n return ''\n}\n"],"names":["AI_STOCK_BUSINESS_NOUNS","english","names","foldedWords","text","normalize","replace","toLowerCase","split","filter","Boolean","NAMES","flatMap","map","name","words","sort","a","b","length","aiStockBusinessNoun","at","found","find","every","word","index"],"mappings":"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"}
|
|
@@ -20,6 +20,21 @@ import { type AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
|
|
|
20
20
|
export declare const AI_LAYOUT_GAP_CODE = "layout-gap";
|
|
21
21
|
/** The gaps a language-built page or frame carries, as the violation that asks again. */
|
|
22
22
|
export declare function aiLayoutGapViolations(nodes: NodesMap): AiDoctrineViolation[];
|
|
23
|
+
/**
|
|
24
|
+
* The page with these sections taken out (AGL-3771), each with any link that
|
|
25
|
+
* scrolled to it: the last answer's sections that still show none of the
|
|
26
|
+
* items they were planned with. A home page refused whole for one such
|
|
27
|
+
* section left a published site with nothing at its address; the section
|
|
28
|
+
* goes, the rest of the page is kept, and the job says which went.
|
|
29
|
+
*/
|
|
30
|
+
export declare function aiLayoutWithoutSections(input: NodesMap, rootId: string, sections: ReadonlyArray<{
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
why: string;
|
|
34
|
+
}>): {
|
|
35
|
+
nodes: NodesMap;
|
|
36
|
+
dropped: string[];
|
|
37
|
+
};
|
|
23
38
|
/** The page or frame with every gap taken out, and what was taken. */
|
|
24
39
|
export declare function aiLayoutWithoutGaps(input: NodesMap, rootId: string): {
|
|
25
40
|
nodes: NodesMap;
|
|
@@ -192,17 +192,67 @@ function cloneNodes(input) {
|
|
|
192
192
|
var _nodes_section_props;
|
|
193
193
|
if (section === first || !nodes[section] || speaks(nodes, section, true)) continue;
|
|
194
194
|
const label = String((_ref1 = (_nodes_section_props = nodes[section].props) == null ? void 0 : _nodes_section_props['ariaLabel']) != null ? _ref1 : section);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
dropped.push(`the section "${label}", left with only its heading`, ...removeSection(nodes, section, remove, prune));
|
|
196
|
+
}
|
|
197
|
+
return dropped;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Takes a section out, and each link that scrolled to it. A page built in the
|
|
201
|
+
* layout language is written whole, so its page step and the site start read
|
|
202
|
+
* a planned section missing from it as one taken out, never one still to
|
|
203
|
+
* build (AGL-3771).
|
|
204
|
+
*/ function removeSection(nodes, section, remove, prune) {
|
|
205
|
+
const dropped = [];
|
|
206
|
+
remove(section);
|
|
207
|
+
for (const [id, node] of Object.entries(nodes)){
|
|
208
|
+
if (nodes[id] && aiStoredScrollTargets(node).includes(section)) {
|
|
209
|
+
dropped.push(`"${wordsOf(node)}", a link to that section`);
|
|
210
|
+
prune(id);
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
return dropped;
|
|
205
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* The page with these sections taken out (AGL-3771), each with any link that
|
|
217
|
+
* scrolled to it: the last answer's sections that still show none of the
|
|
218
|
+
* items they were planned with. A home page refused whole for one such
|
|
219
|
+
* section left a published site with nothing at its address; the section
|
|
220
|
+
* goes, the rest of the page is kept, and the job says which went.
|
|
221
|
+
*/ export function aiLayoutWithoutSections(input, rootId, sections) {
|
|
222
|
+
var _ref;
|
|
223
|
+
var _nodes_rootId;
|
|
224
|
+
const present = sections.filter((section)=>input[section.id]);
|
|
225
|
+
if (!present.length) return {
|
|
226
|
+
nodes: input,
|
|
227
|
+
dropped: []
|
|
228
|
+
};
|
|
229
|
+
const nodes = cloneNodes(input);
|
|
230
|
+
const dropped = [];
|
|
231
|
+
const remove = (id)=>{
|
|
232
|
+
var _nodes_parent_nodes;
|
|
233
|
+
const parent = parentsOf(nodes).get(id);
|
|
234
|
+
for (const each of subtree(nodes, id))delete nodes[each];
|
|
235
|
+
if (parent && nodes[parent]) nodes[parent].nodes = ((_nodes_parent_nodes = nodes[parent].nodes) != null ? _nodes_parent_nodes : []).filter((child)=>child !== id);
|
|
236
|
+
};
|
|
237
|
+
/** A link and each element above it left with nothing to say, short of a section. */ const sectionRoots = new Set((_ref = (_nodes_rootId = nodes[rootId]) == null ? void 0 : _nodes_rootId.nodes) != null ? _ref : []);
|
|
238
|
+
const prune = (id)=>{
|
|
239
|
+
let parent = parentsOf(nodes).get(id);
|
|
240
|
+
remove(id);
|
|
241
|
+
while(parent && parent !== rootId && nodes[parent]){
|
|
242
|
+
if (sectionRoots.has(parent) || speaks(nodes, parent)) break;
|
|
243
|
+
const above = parentsOf(nodes).get(parent);
|
|
244
|
+
remove(parent);
|
|
245
|
+
parent = above;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
for (const section of present){
|
|
249
|
+
dropped.push(`the section "${section.name}", ${section.why}`, ...removeSection(nodes, section.id, remove, prune));
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
nodes: nodes,
|
|
253
|
+
dropped
|
|
254
|
+
};
|
|
255
|
+
}
|
|
206
256
|
/** The page or frame with every gap taken out, and what was taken. */ export function aiLayoutWithoutGaps(input, rootId) {
|
|
207
257
|
const found = gapsOf(input);
|
|
208
258
|
if (!found.length) return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-gaps.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 { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiBracketedFacts, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport { aiStoredScrollTargets } from '../runtime/ai-page-links'\n\n/**\n * No gap on a published page (AGL-3660). A guided start publishes what it\n * builds, so a fact the brief never gave — a phone number, an address, the\n * opening hours — is left out of a page or a frame built in the layout\n * language, never written as the bracketed gap (\"[phone number]\") a draft a\n * person edits first may carry. An answer holding one is asked again; the\n * last answer a generation takes has each one taken out instead: the line\n * that carries it, the row or card it leaves with nothing to say, and a\n * section left with only its heading.\n */\n\ninterface GapNode {\n componentId: string\n props?: Record<string, unknown>\n nodes?: string[]\n}\n\ntype GapNodes = Record<string, GapNode>\n\n/** Props that hold no words a visitor reads: an icon's drawing is path data. */\nconst NOT_WORDS = new Set(['iconPath', 'iconId'])\n\n/** The props a node's own words are in. */\nconst WORD_PROPS = ['children', 'primary', 'secondary', 'label'] as const\n\n/** Elements that say something with no words of their own. */\nconst SPEAKS_ALONE = new Set([\n 'image',\n 'form',\n 'layoutSlot',\n 'reusableInstance',\n 'muiDrawerToggle',\n 'muiDrawer',\n // The site's own records, listed by the elements that keep them (AGL-3676).\n 'product-grid',\n 'collectionEntries',\n 'cart',\n // A music site's player, placed empty for the owner's tracks (AGL-3716).\n 'musicPlayer',\n // The site's events, listed by the events-calendar plugin (AGL-3755).\n 'eventList',\n])\n\nconst HEADING_VARIANTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'overline'])\n\n/** The violation code a gap is named by. */\nexport const AI_LAYOUT_GAP_CODE = 'layout-gap'\n\n/** Every string a value holds, however deep, its icon drawings aside. */\nfunction stringsOf(value: unknown, key = ''): string[] {\n if (NOT_WORDS.has(key)) return []\n if (typeof value === 'string') return [value]\n if (Array.isArray(value)) return value.flatMap((entry) => stringsOf(entry))\n if (value && typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).flatMap(([name, entry]) => stringsOf(entry, name))\n }\n return []\n}\n\n/** Each node whose words carry a gap, with the gaps it carries. */\nfunction gapsOf(nodes: GapNodes): Array<{ id: string; facts: string[]; text: string }> {\n return Object.entries(nodes).flatMap(([id, node]) => {\n const texts = stringsOf(node.props ?? {})\n const facts = aiBracketedFacts(texts)\n return facts.length ? [{ id, facts, text: texts.filter((text) => aiBracketedFacts([text]).length).join(' ') }] : []\n })\n}\n\n/** The gaps a language-built page or frame carries, as the violation that asks again. */\nexport function aiLayoutGapViolations(nodes: NodesMap): AiDoctrineViolation[] {\n const found = gapsOf(nodes as unknown as GapNodes)\n if (!found.length) return []\n const facts = aiBracketedFacts(found.flatMap((entry) => entry.facts))\n return [\n {\n rule: 14,\n code: AI_LAYOUT_GAP_CODE,\n message: `${facts.join(', ')} ${facts.length === 1 ? 'is a gap' : 'are gaps'} for a fact the brief does not give, and this site is published exactly as written. Leave each fact out: write the sentence without it, and make no list line, card or contact row for it. Never write a gap in square brackets, and never invent the fact.`,\n nodeIds: found.map((entry) => entry.id),\n },\n ]\n}\n\nfunction parentsOf(nodes: GapNodes): Map<string, string> {\n const parents = new Map<string, string>()\n for (const [id, node] of Object.entries(nodes)) for (const child of node.nodes ?? []) parents.set(child, id)\n return parents\n}\n\nfunction subtree(nodes: GapNodes, id: string): string[] {\n const ids: string[] = []\n const stack = [id]\n while (stack.length) {\n const next = stack.pop() as string\n if (!nodes[next] || ids.includes(next)) continue\n ids.push(next)\n stack.push(...(nodes[next].nodes ?? []))\n }\n return ids\n}\n\nfunction wordsOf(node: GapNode): string {\n for (const prop of WORD_PROPS) {\n const value = node.props?.[prop]\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return ''\n}\n\nfunction isHeading(node: GapNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n return HEADING_VARIANTS.has(String(node.props?.['variant'] ?? '')) || /^h[1-6]$/.test(String(node.props?.['component'] ?? ''))\n}\n\n/** Whether anything under a node says something; with `beyondHeadings`, anything but a heading. */\nfunction speaks(nodes: GapNodes, id: string, beyondHeadings = false): boolean {\n return subtree(nodes, id).some((each) => {\n const node = nodes[each]\n const says = SPEAKS_ALONE.has(node.componentId) || !!wordsOf(node)\n return says && !(beyondHeadings && isHeading(node))\n })\n}\n\n/** A node's props, its component's prop values copied too, so a rewrite never reaches the input. */\nfunction copyProps(props: Record<string, unknown>): Record<string, unknown> {\n const values = props['propValues']\n return values && typeof values === 'object' && !Array.isArray(values)\n ? { ...props, propValues: { ...(values as Record<string, unknown>) } }\n : { ...props }\n}\n\nfunction cloneNodes(input: NodesMap): GapNodes {\n return Object.fromEntries(\n Object.entries(input as unknown as GapNodes).map(([id, node]) => [\n id,\n {\n ...node,\n ...(node.props ? { props: copyProps(node.props) } : {}),\n ...(node.nodes ? { nodes: [...node.nodes] } : {}),\n },\n ]),\n )\n}\n\n/**\n * Takes each found node out, and each element above it left with nothing to\n * say, up to its section; a section other than the page's first left with\n * only its heading goes too, with any link that scrolled to it.\n */\nfunction pruneFound(nodes: GapNodes, rootId: string, found: ReadonlyArray<{ id: string; text: string }>): string[] {\n const dropped: string[] = []\n const sections = new Set(nodes[rootId]?.nodes ?? [])\n const touched = new Set<string>()\n const remove = (id: string) => {\n const parent = parentsOf(nodes).get(id)\n for (const each of subtree(nodes, id)) delete nodes[each]\n if (parent && nodes[parent]) nodes[parent].nodes = (nodes[parent].nodes ?? []).filter((child) => child !== id)\n }\n /** Takes a node out, and each element above it it leaves with nothing to say, short of a section. */\n const prune = (id: string) => {\n const parents = parentsOf(nodes)\n let section: string | undefined = id\n while (section && !sections.has(section)) section = parents.get(section)\n if (section && section !== id) touched.add(section)\n let parent = parents.get(id)\n remove(id)\n while (parent && parent !== rootId && nodes[parent]) {\n if (sections.has(parent) || speaks(nodes, parent)) break\n const above = parentsOf(nodes).get(parent)\n remove(parent)\n parent = above\n }\n }\n for (const entry of found) {\n if (!nodes[entry.id]) continue\n dropped.push(entry.text)\n prune(entry.id)\n }\n const first = nodes[rootId]?.nodes?.[0]\n for (const section of touched) {\n if (section === first || !nodes[section] || speaks(nodes, section, true)) continue\n const label = String(nodes[section].props?.['ariaLabel'] ?? section)\n remove(section)\n dropped.push(`the section \"${label}\", left with only its heading`)\n for (const [id, node] of Object.entries(nodes)) {\n if (nodes[id] && aiStoredScrollTargets(node).includes(section)) {\n dropped.push(`\"${wordsOf(node)}\", a link to that section`)\n prune(id)\n }\n }\n }\n return dropped\n}\n\n/** The page or frame with every gap taken out, and what was taken. */\nexport function aiLayoutWithoutGaps(input: NodesMap, rootId: string): { nodes: NodesMap; dropped: string[] } {\n const found = gapsOf(input as unknown as GapNodes)\n if (!found.length) return { nodes: input, dropped: [] }\n const nodes = cloneNodes(input)\n const dropped = pruneFound(nodes, rootId, found)\n return { nodes: nodes as unknown as NodesMap, dropped }\n}\n\n// ── Internal references ─────────────────────────────────────────────────\n\n/**\n * What a link may name, which never belongs in words a visitor reads: the\n * site's pages, with the title a reference to one is written as, and every\n * other id the page or frame was built against (its forms, components and\n * sections).\n */\nexport interface AiLayoutRefNames {\n pages: ReadonlyArray<{ id: string; label: string }>\n ids: readonly string[]\n}\n\n/** The violation code an internal reference in copy is named by. */\nexport const AI_LAYOUT_REF_CODE = 'layout-internal-ref'\n\n/** A link's own notation, or a template token other than the site's own (`{{host.…}}`). */\nconst REF = /\\b(?:page|new|form|component|screen):[A-Za-z0-9_-]+|\\{\\{(?!\\s*host\\.)[^{}]*\\}\\}/g\nconst PAGE_REF = /\\bpage:([A-Za-z0-9_-]+)/g\n\n/** The props whose words a visitor reads, and a component's prop values. */\nconst SHOWN_PROPS = [...WORD_PROPS, 'alt', 'title']\n\nfunction shownTexts(node: GapNode): Array<{ get: () => string; set: (value: string) => void }> {\n const props = node.props ?? {}\n const slots: Array<{ get: () => string; set: (value: string) => void }> = []\n for (const prop of SHOWN_PROPS) {\n if (typeof props[prop] === 'string') slots.push({ get: () => props[prop] as string, set: (value) => (props[prop] = value) })\n }\n const values = props['propValues']\n if (values && typeof values === 'object' && !Array.isArray(values)) {\n const record = values as Record<string, unknown>\n for (const name of Object.keys(record)) {\n if (typeof record[name] === 'string') slots.push({ get: () => record[name] as string, set: (value) => (record[name] = value) })\n }\n }\n return slots\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction refsIn(text: string, names: AiLayoutRefNames): string[] {\n const found = (text.match(REF) ?? []).map((ref) => ref.trim())\n for (const id of names.ids) {\n if (id.length >= 6 && new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(id)}($|[^A-Za-z0-9_-])`).test(text)) found.push(id)\n }\n return [...new Set(found)]\n}\n\n/** An entry's own token, which a collection repeater fills per entry when the page renders. */\nconst ENTRY_TOKEN = /\\{\\{\\s*entry\\.[A-Za-z]+\\s*\\}\\}/g\n\n/** Every node under a Collection Entries block: its template, repeated once per entry (AGL-3676). */\nfunction repeatedByEntries(nodes: GapNodes): Set<string> {\n const repeated = new Set<string>()\n for (const [id, node] of Object.entries(nodes)) {\n if (node.componentId !== 'collectionEntries') continue\n for (const each of subtree(nodes, id)) if (each !== id) repeated.add(each)\n }\n return repeated\n}\n\n/** A record's own token, which a repeat over a dataset fills per record when the page renders. */\nconst ITEM_TOKEN = /\\{\\{\\s*item\\.[A-Za-z][A-Za-z0-9_]*(?:\\.[A-Za-z][A-Za-z0-9_]*)?\\s*\\}\\}/g\n\n/** Every node a repeat over a dataset copies once per record (AGL-3616), the repeating node included. */\nfunction repeatedByDataset(nodes: GapNodes): Set<string> {\n const repeated = new Set<string>()\n for (const [id, node] of Object.entries(nodes)) {\n const key = node.props?.['repeatDataset']\n if (typeof key !== 'string' || !key.trim()) continue\n for (const each of subtree(nodes, id)) repeated.add(each)\n }\n return repeated\n}\n\nfunction refsOf(nodes: GapNodes, names: AiLayoutRefNames): Array<{ id: string; refs: string[]; text: string }> {\n // A post's card names its post by the entry's tokens, which the page fills\n // per post: words a visitor reads as the post's own, never a reference.\n // A dataset's card names its record by the record's, the same way (AGL-3616).\n const repeated = repeatedByEntries(nodes)\n const records = repeatedByDataset(nodes)\n return Object.entries(nodes).flatMap(([id, node]) => {\n const texts = shownTexts(node).map((slot) => {\n const text = repeated.has(id) ? slot.get().replace(ENTRY_TOKEN, '') : slot.get()\n return records.has(id) ? text.replace(ITEM_TOKEN, '') : text\n })\n const refs = [...new Set(texts.flatMap((text) => refsIn(text, names)))]\n return refs.length ? [{ id, refs, text: texts.filter((text) => refsIn(text, names).length).join(' ') }] : []\n })\n}\n\n/** Internal references in a language-built page's or frame's words, as the violation that asks again. */\nexport function aiLayoutRefViolations(nodes: NodesMap, names: AiLayoutRefNames): AiDoctrineViolation[] {\n const found = refsOf(nodes as unknown as GapNodes, names)\n if (!found.length) return []\n const refs = [...new Set(found.flatMap((entry) => entry.refs))].map((ref) => `\"${ref}\"`)\n return [\n {\n rule: 14,\n code: AI_LAYOUT_REF_CODE,\n message: `${refs.join(', ')} ${refs.length === 1 ? 'is a link reference' : 'are link references'} written into words a visitor reads. A reference goes only in a button's or a list item's to; in text, name the page by its title, or leave the reference out.`,\n nodeIds: found.map((entry) => entry.id),\n },\n ]\n}\n\n/**\n * The page or frame with no internal reference in its words: a page's\n * reference is written as that page's title, and a line that still names a\n * reference is taken out, as a gap is. Returns what was rewritten and taken.\n */\nexport function aiLayoutWithoutRefs(\n input: NodesMap,\n rootId: string,\n names: AiLayoutRefNames,\n): { nodes: NodesMap; dropped: string[] } {\n if (!refsOf(input as unknown as GapNodes, names).length) return { nodes: input, dropped: [] }\n const nodes = cloneNodes(input)\n const labels = new Map(names.pages.map((page) => [page.id, page.label]))\n const dropped: string[] = []\n for (const node of Object.values(nodes)) {\n for (const slot of shownTexts(node)) {\n const before = slot.get()\n const after = before.replace(PAGE_REF, (ref, id: string) => labels.get(id) ?? ref)\n if (after !== before) {\n slot.set(after)\n dropped.push(`\"${before}\" written as \"${after}\"`)\n }\n }\n }\n dropped.push(...pruneFound(nodes, rootId, refsOf(nodes, names)))\n return { nodes: nodes as unknown as NodesMap, dropped }\n}\n\n/**\n * A language-built page's or frame's words held to both (AGL-3660): before\n * the last answer, each gap and each internal reference is a violation that\n * asks again; in the last, they are taken out and the rest is kept.\n */\nexport function aiLayoutCopyCheck(\n input: NodesMap,\n rootId: string,\n names: AiLayoutRefNames,\n last: boolean,\n): { nodes: NodesMap; dropped: string[]; violations: AiDoctrineViolation[] } {\n if (!last) {\n return { nodes: input, dropped: [], violations: [...aiLayoutRefViolations(input, names), ...aiLayoutGapViolations(input)] }\n }\n const refs = aiLayoutWithoutRefs(input, rootId, names)\n const gaps = aiLayoutWithoutGaps(refs.nodes, rootId)\n return { nodes: gaps.nodes, dropped: [...refs.dropped, ...gaps.dropped], violations: [] }\n}\n"],"names":["aiBracketedFacts","aiStoredScrollTargets","NOT_WORDS","Set","WORD_PROPS","SPEAKS_ALONE","HEADING_VARIANTS","AI_LAYOUT_GAP_CODE","stringsOf","value","key","has","Array","isArray","flatMap","entry","Object","entries","name","gapsOf","nodes","id","node","texts","props","facts","length","text","filter","join","aiLayoutGapViolations","found","rule","code","message","nodeIds","map","parentsOf","parents","Map","child","set","subtree","ids","stack","next","pop","includes","push","wordsOf","prop","trim","isHeading","componentId","String","test","speaks","beyondHeadings","some","each","says","copyProps","values","propValues","cloneNodes","input","fromEntries","pruneFound","rootId","dropped","sections","touched","remove","parent","get","prune","section","add","above","first","label","aiLayoutWithoutGaps","AI_LAYOUT_REF_CODE","REF","PAGE_REF","SHOWN_PROPS","shownTexts","slots","record","keys","escapeRegExp","replace","refsIn","names","match","ref","RegExp","ENTRY_TOKEN","repeatedByEntries","repeated","ITEM_TOKEN","repeatedByDataset","refsOf","records","slot","refs","aiLayoutRefViolations","aiLayoutWithoutRefs","labels","pages","page","before","after","aiLayoutCopyCheck","last","violations","gaps"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/layout-language/ai-layout-gaps.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 { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport { aiBracketedFacts, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport { aiStoredScrollTargets } from '../runtime/ai-page-links'\n\n/**\n * No gap on a published page (AGL-3660). A guided start publishes what it\n * builds, so a fact the brief never gave — a phone number, an address, the\n * opening hours — is left out of a page or a frame built in the layout\n * language, never written as the bracketed gap (\"[phone number]\") a draft a\n * person edits first may carry. An answer holding one is asked again; the\n * last answer a generation takes has each one taken out instead: the line\n * that carries it, the row or card it leaves with nothing to say, and a\n * section left with only its heading.\n */\n\ninterface GapNode {\n componentId: string\n props?: Record<string, unknown>\n nodes?: string[]\n}\n\ntype GapNodes = Record<string, GapNode>\n\n/** Props that hold no words a visitor reads: an icon's drawing is path data. */\nconst NOT_WORDS = new Set(['iconPath', 'iconId'])\n\n/** The props a node's own words are in. */\nconst WORD_PROPS = ['children', 'primary', 'secondary', 'label'] as const\n\n/** Elements that say something with no words of their own. */\nconst SPEAKS_ALONE = new Set([\n 'image',\n 'form',\n 'layoutSlot',\n 'reusableInstance',\n 'muiDrawerToggle',\n 'muiDrawer',\n // The site's own records, listed by the elements that keep them (AGL-3676).\n 'product-grid',\n 'collectionEntries',\n 'cart',\n // A music site's player, placed empty for the owner's tracks (AGL-3716).\n 'musicPlayer',\n // The site's events, listed by the events-calendar plugin (AGL-3755).\n 'eventList',\n])\n\nconst HEADING_VARIANTS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'overline'])\n\n/** The violation code a gap is named by. */\nexport const AI_LAYOUT_GAP_CODE = 'layout-gap'\n\n/** Every string a value holds, however deep, its icon drawings aside. */\nfunction stringsOf(value: unknown, key = ''): string[] {\n if (NOT_WORDS.has(key)) return []\n if (typeof value === 'string') return [value]\n if (Array.isArray(value)) return value.flatMap((entry) => stringsOf(entry))\n if (value && typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).flatMap(([name, entry]) => stringsOf(entry, name))\n }\n return []\n}\n\n/** Each node whose words carry a gap, with the gaps it carries. */\nfunction gapsOf(nodes: GapNodes): Array<{ id: string; facts: string[]; text: string }> {\n return Object.entries(nodes).flatMap(([id, node]) => {\n const texts = stringsOf(node.props ?? {})\n const facts = aiBracketedFacts(texts)\n return facts.length ? [{ id, facts, text: texts.filter((text) => aiBracketedFacts([text]).length).join(' ') }] : []\n })\n}\n\n/** The gaps a language-built page or frame carries, as the violation that asks again. */\nexport function aiLayoutGapViolations(nodes: NodesMap): AiDoctrineViolation[] {\n const found = gapsOf(nodes as unknown as GapNodes)\n if (!found.length) return []\n const facts = aiBracketedFacts(found.flatMap((entry) => entry.facts))\n return [\n {\n rule: 14,\n code: AI_LAYOUT_GAP_CODE,\n message: `${facts.join(', ')} ${facts.length === 1 ? 'is a gap' : 'are gaps'} for a fact the brief does not give, and this site is published exactly as written. Leave each fact out: write the sentence without it, and make no list line, card or contact row for it. Never write a gap in square brackets, and never invent the fact.`,\n nodeIds: found.map((entry) => entry.id),\n },\n ]\n}\n\nfunction parentsOf(nodes: GapNodes): Map<string, string> {\n const parents = new Map<string, string>()\n for (const [id, node] of Object.entries(nodes)) for (const child of node.nodes ?? []) parents.set(child, id)\n return parents\n}\n\nfunction subtree(nodes: GapNodes, id: string): string[] {\n const ids: string[] = []\n const stack = [id]\n while (stack.length) {\n const next = stack.pop() as string\n if (!nodes[next] || ids.includes(next)) continue\n ids.push(next)\n stack.push(...(nodes[next].nodes ?? []))\n }\n return ids\n}\n\nfunction wordsOf(node: GapNode): string {\n for (const prop of WORD_PROPS) {\n const value = node.props?.[prop]\n if (typeof value === 'string' && value.trim()) return value.trim()\n }\n return ''\n}\n\nfunction isHeading(node: GapNode): boolean {\n if (node.componentId !== 'muiTypography') return false\n return HEADING_VARIANTS.has(String(node.props?.['variant'] ?? '')) || /^h[1-6]$/.test(String(node.props?.['component'] ?? ''))\n}\n\n/** Whether anything under a node says something; with `beyondHeadings`, anything but a heading. */\nfunction speaks(nodes: GapNodes, id: string, beyondHeadings = false): boolean {\n return subtree(nodes, id).some((each) => {\n const node = nodes[each]\n const says = SPEAKS_ALONE.has(node.componentId) || !!wordsOf(node)\n return says && !(beyondHeadings && isHeading(node))\n })\n}\n\n/** A node's props, its component's prop values copied too, so a rewrite never reaches the input. */\nfunction copyProps(props: Record<string, unknown>): Record<string, unknown> {\n const values = props['propValues']\n return values && typeof values === 'object' && !Array.isArray(values)\n ? { ...props, propValues: { ...(values as Record<string, unknown>) } }\n : { ...props }\n}\n\nfunction cloneNodes(input: NodesMap): GapNodes {\n return Object.fromEntries(\n Object.entries(input as unknown as GapNodes).map(([id, node]) => [\n id,\n {\n ...node,\n ...(node.props ? { props: copyProps(node.props) } : {}),\n ...(node.nodes ? { nodes: [...node.nodes] } : {}),\n },\n ]),\n )\n}\n\n/**\n * Takes each found node out, and each element above it left with nothing to\n * say, up to its section; a section other than the page's first left with\n * only its heading goes too, with any link that scrolled to it.\n */\nfunction pruneFound(nodes: GapNodes, rootId: string, found: ReadonlyArray<{ id: string; text: string }>): string[] {\n const dropped: string[] = []\n const sections = new Set(nodes[rootId]?.nodes ?? [])\n const touched = new Set<string>()\n const remove = (id: string) => {\n const parent = parentsOf(nodes).get(id)\n for (const each of subtree(nodes, id)) delete nodes[each]\n if (parent && nodes[parent]) nodes[parent].nodes = (nodes[parent].nodes ?? []).filter((child) => child !== id)\n }\n /** Takes a node out, and each element above it it leaves with nothing to say, short of a section. */\n const prune = (id: string) => {\n const parents = parentsOf(nodes)\n let section: string | undefined = id\n while (section && !sections.has(section)) section = parents.get(section)\n if (section && section !== id) touched.add(section)\n let parent = parents.get(id)\n remove(id)\n while (parent && parent !== rootId && nodes[parent]) {\n if (sections.has(parent) || speaks(nodes, parent)) break\n const above = parentsOf(nodes).get(parent)\n remove(parent)\n parent = above\n }\n }\n for (const entry of found) {\n if (!nodes[entry.id]) continue\n dropped.push(entry.text)\n prune(entry.id)\n }\n const first = nodes[rootId]?.nodes?.[0]\n for (const section of touched) {\n if (section === first || !nodes[section] || speaks(nodes, section, true)) continue\n const label = String(nodes[section].props?.['ariaLabel'] ?? section)\n dropped.push(`the section \"${label}\", left with only its heading`, ...removeSection(nodes, section, remove, prune))\n }\n return dropped\n}\n\n/**\n * Takes a section out, and each link that scrolled to it. A page built in the\n * layout language is written whole, so its page step and the site start read\n * a planned section missing from it as one taken out, never one still to\n * build (AGL-3771).\n */\nfunction removeSection(nodes: GapNodes, section: string, remove: (id: string) => void, prune: (id: string) => void): string[] {\n const dropped: string[] = []\n remove(section)\n for (const [id, node] of Object.entries(nodes)) {\n if (nodes[id] && aiStoredScrollTargets(node).includes(section)) {\n dropped.push(`\"${wordsOf(node)}\", a link to that section`)\n prune(id)\n }\n }\n return dropped\n}\n\n/**\n * The page with these sections taken out (AGL-3771), each with any link that\n * scrolled to it: the last answer's sections that still show none of the\n * items they were planned with. A home page refused whole for one such\n * section left a published site with nothing at its address; the section\n * goes, the rest of the page is kept, and the job says which went.\n */\nexport function aiLayoutWithoutSections(\n input: NodesMap,\n rootId: string,\n sections: ReadonlyArray<{ id: string; name: string; why: string }>,\n): { nodes: NodesMap; dropped: string[] } {\n const present = sections.filter((section) => (input as unknown as GapNodes)[section.id])\n if (!present.length) return { nodes: input, dropped: [] }\n const nodes = cloneNodes(input)\n const dropped: string[] = []\n const remove = (id: string) => {\n const parent = parentsOf(nodes).get(id)\n for (const each of subtree(nodes, id)) delete nodes[each]\n if (parent && nodes[parent]) nodes[parent].nodes = (nodes[parent].nodes ?? []).filter((child) => child !== id)\n }\n /** A link and each element above it left with nothing to say, short of a section. */\n const sectionRoots = new Set(nodes[rootId]?.nodes ?? [])\n const prune = (id: string) => {\n let parent = parentsOf(nodes).get(id)\n remove(id)\n while (parent && parent !== rootId && nodes[parent]) {\n if (sectionRoots.has(parent) || speaks(nodes, parent)) break\n const above = parentsOf(nodes).get(parent)\n remove(parent)\n parent = above\n }\n }\n for (const section of present) {\n dropped.push(`the section \"${section.name}\", ${section.why}`, ...removeSection(nodes, section.id, remove, prune))\n }\n return { nodes: nodes as unknown as NodesMap, dropped }\n}\n\n/** The page or frame with every gap taken out, and what was taken. */\nexport function aiLayoutWithoutGaps(input: NodesMap, rootId: string): { nodes: NodesMap; dropped: string[] } {\n const found = gapsOf(input as unknown as GapNodes)\n if (!found.length) return { nodes: input, dropped: [] }\n const nodes = cloneNodes(input)\n const dropped = pruneFound(nodes, rootId, found)\n return { nodes: nodes as unknown as NodesMap, dropped }\n}\n\n// ── Internal references ─────────────────────────────────────────────────\n\n/**\n * What a link may name, which never belongs in words a visitor reads: the\n * site's pages, with the title a reference to one is written as, and every\n * other id the page or frame was built against (its forms, components and\n * sections).\n */\nexport interface AiLayoutRefNames {\n pages: ReadonlyArray<{ id: string; label: string }>\n ids: readonly string[]\n}\n\n/** The violation code an internal reference in copy is named by. */\nexport const AI_LAYOUT_REF_CODE = 'layout-internal-ref'\n\n/** A link's own notation, or a template token other than the site's own (`{{host.…}}`). */\nconst REF = /\\b(?:page|new|form|component|screen):[A-Za-z0-9_-]+|\\{\\{(?!\\s*host\\.)[^{}]*\\}\\}/g\nconst PAGE_REF = /\\bpage:([A-Za-z0-9_-]+)/g\n\n/** The props whose words a visitor reads, and a component's prop values. */\nconst SHOWN_PROPS = [...WORD_PROPS, 'alt', 'title']\n\nfunction shownTexts(node: GapNode): Array<{ get: () => string; set: (value: string) => void }> {\n const props = node.props ?? {}\n const slots: Array<{ get: () => string; set: (value: string) => void }> = []\n for (const prop of SHOWN_PROPS) {\n if (typeof props[prop] === 'string') slots.push({ get: () => props[prop] as string, set: (value) => (props[prop] = value) })\n }\n const values = props['propValues']\n if (values && typeof values === 'object' && !Array.isArray(values)) {\n const record = values as Record<string, unknown>\n for (const name of Object.keys(record)) {\n if (typeof record[name] === 'string') slots.push({ get: () => record[name] as string, set: (value) => (record[name] = value) })\n }\n }\n return slots\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction refsIn(text: string, names: AiLayoutRefNames): string[] {\n const found = (text.match(REF) ?? []).map((ref) => ref.trim())\n for (const id of names.ids) {\n if (id.length >= 6 && new RegExp(`(^|[^A-Za-z0-9_-])${escapeRegExp(id)}($|[^A-Za-z0-9_-])`).test(text)) found.push(id)\n }\n return [...new Set(found)]\n}\n\n/** An entry's own token, which a collection repeater fills per entry when the page renders. */\nconst ENTRY_TOKEN = /\\{\\{\\s*entry\\.[A-Za-z]+\\s*\\}\\}/g\n\n/** Every node under a Collection Entries block: its template, repeated once per entry (AGL-3676). */\nfunction repeatedByEntries(nodes: GapNodes): Set<string> {\n const repeated = new Set<string>()\n for (const [id, node] of Object.entries(nodes)) {\n if (node.componentId !== 'collectionEntries') continue\n for (const each of subtree(nodes, id)) if (each !== id) repeated.add(each)\n }\n return repeated\n}\n\n/** A record's own token, which a repeat over a dataset fills per record when the page renders. */\nconst ITEM_TOKEN = /\\{\\{\\s*item\\.[A-Za-z][A-Za-z0-9_]*(?:\\.[A-Za-z][A-Za-z0-9_]*)?\\s*\\}\\}/g\n\n/** Every node a repeat over a dataset copies once per record (AGL-3616), the repeating node included. */\nfunction repeatedByDataset(nodes: GapNodes): Set<string> {\n const repeated = new Set<string>()\n for (const [id, node] of Object.entries(nodes)) {\n const key = node.props?.['repeatDataset']\n if (typeof key !== 'string' || !key.trim()) continue\n for (const each of subtree(nodes, id)) repeated.add(each)\n }\n return repeated\n}\n\nfunction refsOf(nodes: GapNodes, names: AiLayoutRefNames): Array<{ id: string; refs: string[]; text: string }> {\n // A post's card names its post by the entry's tokens, which the page fills\n // per post: words a visitor reads as the post's own, never a reference.\n // A dataset's card names its record by the record's, the same way (AGL-3616).\n const repeated = repeatedByEntries(nodes)\n const records = repeatedByDataset(nodes)\n return Object.entries(nodes).flatMap(([id, node]) => {\n const texts = shownTexts(node).map((slot) => {\n const text = repeated.has(id) ? slot.get().replace(ENTRY_TOKEN, '') : slot.get()\n return records.has(id) ? text.replace(ITEM_TOKEN, '') : text\n })\n const refs = [...new Set(texts.flatMap((text) => refsIn(text, names)))]\n return refs.length ? [{ id, refs, text: texts.filter((text) => refsIn(text, names).length).join(' ') }] : []\n })\n}\n\n/** Internal references in a language-built page's or frame's words, as the violation that asks again. */\nexport function aiLayoutRefViolations(nodes: NodesMap, names: AiLayoutRefNames): AiDoctrineViolation[] {\n const found = refsOf(nodes as unknown as GapNodes, names)\n if (!found.length) return []\n const refs = [...new Set(found.flatMap((entry) => entry.refs))].map((ref) => `\"${ref}\"`)\n return [\n {\n rule: 14,\n code: AI_LAYOUT_REF_CODE,\n message: `${refs.join(', ')} ${refs.length === 1 ? 'is a link reference' : 'are link references'} written into words a visitor reads. A reference goes only in a button's or a list item's to; in text, name the page by its title, or leave the reference out.`,\n nodeIds: found.map((entry) => entry.id),\n },\n ]\n}\n\n/**\n * The page or frame with no internal reference in its words: a page's\n * reference is written as that page's title, and a line that still names a\n * reference is taken out, as a gap is. Returns what was rewritten and taken.\n */\nexport function aiLayoutWithoutRefs(\n input: NodesMap,\n rootId: string,\n names: AiLayoutRefNames,\n): { nodes: NodesMap; dropped: string[] } {\n if (!refsOf(input as unknown as GapNodes, names).length) return { nodes: input, dropped: [] }\n const nodes = cloneNodes(input)\n const labels = new Map(names.pages.map((page) => [page.id, page.label]))\n const dropped: string[] = []\n for (const node of Object.values(nodes)) {\n for (const slot of shownTexts(node)) {\n const before = slot.get()\n const after = before.replace(PAGE_REF, (ref, id: string) => labels.get(id) ?? ref)\n if (after !== before) {\n slot.set(after)\n dropped.push(`\"${before}\" written as \"${after}\"`)\n }\n }\n }\n dropped.push(...pruneFound(nodes, rootId, refsOf(nodes, names)))\n return { nodes: nodes as unknown as NodesMap, dropped }\n}\n\n/**\n * A language-built page's or frame's words held to both (AGL-3660): before\n * the last answer, each gap and each internal reference is a violation that\n * asks again; in the last, they are taken out and the rest is kept.\n */\nexport function aiLayoutCopyCheck(\n input: NodesMap,\n rootId: string,\n names: AiLayoutRefNames,\n last: boolean,\n): { nodes: NodesMap; dropped: string[]; violations: AiDoctrineViolation[] } {\n if (!last) {\n return { nodes: input, dropped: [], violations: [...aiLayoutRefViolations(input, names), ...aiLayoutGapViolations(input)] }\n }\n const refs = aiLayoutWithoutRefs(input, rootId, names)\n const gaps = aiLayoutWithoutGaps(refs.nodes, rootId)\n return { nodes: gaps.nodes, dropped: [...refs.dropped, ...gaps.dropped], violations: [] }\n}\n"],"names":["aiBracketedFacts","aiStoredScrollTargets","NOT_WORDS","Set","WORD_PROPS","SPEAKS_ALONE","HEADING_VARIANTS","AI_LAYOUT_GAP_CODE","stringsOf","value","key","has","Array","isArray","flatMap","entry","Object","entries","name","gapsOf","nodes","id","node","texts","props","facts","length","text","filter","join","aiLayoutGapViolations","found","rule","code","message","nodeIds","map","parentsOf","parents","Map","child","set","subtree","ids","stack","next","pop","includes","push","wordsOf","prop","trim","isHeading","componentId","String","test","speaks","beyondHeadings","some","each","says","copyProps","values","propValues","cloneNodes","input","fromEntries","pruneFound","rootId","dropped","sections","touched","remove","parent","get","prune","section","add","above","first","label","removeSection","aiLayoutWithoutSections","present","sectionRoots","why","aiLayoutWithoutGaps","AI_LAYOUT_REF_CODE","REF","PAGE_REF","SHOWN_PROPS","shownTexts","slots","record","keys","escapeRegExp","replace","refsIn","names","match","ref","RegExp","ENTRY_TOKEN","repeatedByEntries","repeated","ITEM_TOKEN","repeatedByDataset","refsOf","records","slot","refs","aiLayoutRefViolations","aiLayoutWithoutRefs","labels","pages","page","before","after","aiLayoutCopyCheck","last","violations","gaps"],"mappings":"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"}
|
|
@@ -228,9 +228,13 @@ export interface AiBuildPlan {
|
|
|
228
228
|
* is in (AGL-3760): two to four words a stock photo library is searched by,
|
|
229
229
|
* the business's subject first — "bakery bread pastries" for "una panadería
|
|
230
230
|
* … con pan dulce, conchas". The libraries are tagged in English, so a site
|
|
231
|
-
* in another language searches by these rather than its own words
|
|
232
|
-
*
|
|
233
|
-
*
|
|
231
|
+
* in another language searches by these rather than its own words — unless
|
|
232
|
+
* its business type names a business the curated table knows in English
|
|
233
|
+
* (`aiStockBusinessNoun`), which leads — and its plan is asked for them
|
|
234
|
+
* again once when it gave none (AGL-3779). Absent on every other kind's
|
|
235
|
+
* plan, on one that said null, on one whose words were not plain English
|
|
236
|
+
* letters, and on a plan in another language whose re-ask still gave none,
|
|
237
|
+
* whose photos then fall back to its kind of site.
|
|
234
238
|
*/
|
|
235
239
|
photoWords?: string;
|
|
236
240
|
}
|
|
@@ -451,7 +451,17 @@ const LANGUAGE_SCHEMA = nullableString("The language the brief is written in, wh
|
|
|
451
451
|
const words = text.split(/[\s,&]+/).map((word)=>word.replace(/^[-']+|[-']+$/g, '')).filter((word)=>word.length > 1);
|
|
452
452
|
return words.length ? words.slice(0, AI_PLAN_PHOTO_WORDS_MAX).join(' ') : null;
|
|
453
453
|
}
|
|
454
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Required whenever the plan's language is not English (AGL-3779), the field
|
|
456
|
+
* beside it. Asked "for a brief not in English", two Spanish bakeries' plans
|
|
457
|
+
* said `es` and gave none (jobs ih6kVvCGP1, iGmqrEgyW4): a guided start's
|
|
458
|
+
* brief is English sentences around the person's own words. A strict schema
|
|
459
|
+
* has no conditional keyword, so the condition is said here and held by the
|
|
460
|
+
* plan step (`aiSitePhotoWordsCheck`), which re-asks once. The kind of
|
|
461
|
+
* business comes first because the first word is the subject every photo is
|
|
462
|
+
* searched by (`aiStockPhotoWordsSubject`): asked for the most specific word
|
|
463
|
+
* first, a florist's live plan wrote "wedding bouquets event flowers".
|
|
464
|
+
*/ const PHOTO_WORDS_SCHEMA = nullableString('Required when language is not en: 2-4 English words, the kind of business first, then what it sells, e.g. florist wedding bouquets; never a place or the business name. Null when language is en.');
|
|
455
465
|
/**
|
|
456
466
|
* Any plan tool with the site's language (AGL-3750) and its photo words in
|
|
457
467
|
* English (AGL-3760), offered only to a `site` job, whose guided start sets
|