@fasl-work/caos-app-shell 0.12.0 → 0.13.1
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/bin/caos-shell-gate.mjs +1 -1
- package/dist/chart.js +2 -2
- package/dist/{chunk-X6Y5CDUD.js → chunk-33WTUQ7Q.js} +5 -3
- package/dist/{chunk-DX36XRD5.js → chunk-YECZXGVG.js} +1 -1
- package/dist/index.d.ts +155 -57
- package/dist/index.js +1032 -212
- package/dist/{keys-CnvqkckF.d.ts → keys-DDQGijwQ.d.ts} +10 -4
- package/dist/keys.d.ts +1 -1
- package/dist/keys.js +1 -1
- package/gate/areas.mjs +230 -54
- package/gate/inpage.mjs +52 -10
- package/gate/run.mjs +33 -8
- package/gate/serve.mjs +6 -2
- package/package.json +1 -1
- package/reserved-classes.json +56 -18
- package/src/content/executive.schema.json +659 -141
- package/styles.css +37 -18
|
@@ -110,10 +110,16 @@ declare function AppShell({ config, children }: {
|
|
|
110
110
|
children: ReactNode;
|
|
111
111
|
}): react.JSX.Element;
|
|
112
112
|
|
|
113
|
+
/** The route every product lands on: the Introduction, whose executive view opens first (shell 0.13.0). */
|
|
114
|
+
declare const LANDING_ROUTE = "/introduction";
|
|
115
|
+
/** The route of the App (the tool); it was the root before 0.13.0 (the `Landing` component forwards old links). */
|
|
116
|
+
declare const APP_ROUTE = "/app";
|
|
113
117
|
/**
|
|
114
|
-
* The six routes every CAOS product carries, in this order (
|
|
115
|
-
*
|
|
116
|
-
*
|
|
118
|
+
* The six routes every CAOS product carries, ALWAYS in this order (Felipe, 2026-10-13): App, Introduction,
|
|
119
|
+
* Methodology, Implementation, Experiments, Benchmark. The App stays the leftmost tab; the product lands on the
|
|
120
|
+
* Introduction (its executive view first) through `Landing`, which never changes the order (0.13.0 had moved the
|
|
121
|
+
* Introduction first; 0.13.1 restores the App). One definition, so the header, the router and the gate read the same
|
|
122
|
+
* list and no product retypes it.
|
|
117
123
|
*/
|
|
118
124
|
declare const STANDARD_ROUTES: ShellRoute[];
|
|
119
125
|
|
|
@@ -132,4 +138,4 @@ declare const LANG_STORAGE_KEY = "caos.lang";
|
|
|
132
138
|
*/
|
|
133
139
|
declare const THEME_BOOT_SCRIPT: string;
|
|
134
140
|
|
|
135
|
-
export {
|
|
141
|
+
export { APP_ROUTE as A, LANDING_ROUTE as L, STANDARD_ROUTES as S, THEME_BOOT_SCRIPT as T, AppShell as a, type ArchTab as b, type ArchitectureConfig as c, ArchitectureModal as d, LANG_STORAGE_KEY as e, type ShellConfig as f, type ShellRoute as g, THEME_STORAGE_KEY as h, validateArchitectureConfig as v };
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { L as LANG_STORAGE_KEY, S as STANDARD_ROUTES, T as THEME_BOOT_SCRIPT,
|
|
1
|
+
export { A as APP_ROUTE, L as LANDING_ROUTE, e as LANG_STORAGE_KEY, S as STANDARD_ROUTES, T as THEME_BOOT_SCRIPT, h as THEME_STORAGE_KEY } from './keys-DDQGijwQ.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import './text-BlY47nIt.js';
|
|
4
4
|
import 'zustand';
|
package/dist/keys.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-
|
|
1
|
+
export { APP_ROUTE, LANDING_ROUTE, LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-33WTUQ7Q.js';
|
package/gate/areas.mjs
CHANGED
|
@@ -170,39 +170,79 @@ export const EXECUTIVE_FLOORS = ['executive', 'executive-view', 'initiative', 'o
|
|
|
170
170
|
|
|
171
171
|
// ------------------------------------------------------------------------------------------------ the executive view
|
|
172
172
|
// The executive view of the Introduction is the shell's ExecutiveView fed by the product's content/executive.json
|
|
173
|
-
// (conventions/problem-context-first.md). Its structure
|
|
174
|
-
// here AND in `src/content/executive.ts` (test/executive.test.ts keeps
|
|
175
|
-
// page, the component validates the data.
|
|
176
|
-
export const EXEC_SCHEMA_VERSION = '
|
|
173
|
+
// (conventions/problem-context-first.md). Its structure and its floors are `src/content/executive.schema.json`; its writing
|
|
174
|
+
// limits, its floors and its 4 C's are here AND in `src/content/executive.ts` (test/executive.test.ts keeps them equal): the
|
|
175
|
+
// gate measures the rendered page, the component validates the data.
|
|
176
|
+
export const EXEC_SCHEMA_VERSION = '2.0.0';
|
|
177
177
|
export const EXEC_LIMITS = {
|
|
178
178
|
maxWords: 30,
|
|
179
|
-
|
|
179
|
+
minParagraphWords: 25,
|
|
180
180
|
equation: /\$|\\\(|\\\[|\\[a-zA-Z]+/,
|
|
181
181
|
citations: [/\(\s*Source/i, /\[\d+\]/, /\(\s*ECB/, /\(\w[^()]*\d{4}\)/],
|
|
182
182
|
minLabelPx: 9,
|
|
183
|
+
fileCitations: [/\bfile\s*\d/i, /\barchivo\s*\d/i, /\.pdf\b/i, /\bPublished on\b/i],
|
|
184
|
+
promotional: [/\bour\s+clients?\b/i, /\bnuestros?\s+clientes?\b/i, /\bcontact\s+us\b/i, /\bcontáctenos\b/i, /\bbuy\b/i, /\bcomprar\b/i, /\bprices?\b/i, /\bprecios?\b/i, /\boffers?\b/i, /\bofertas?\b/i],
|
|
183
185
|
};
|
|
184
|
-
/** The
|
|
186
|
+
/** The floors of the view as [min, max] counts of rows; a copy of EXEC_FLOORS of src/content/executive.ts (and of the schema's minItems and maxItems). */
|
|
187
|
+
export const EXEC_FLOORS = {
|
|
188
|
+
headline_figures: [2, 3],
|
|
189
|
+
'pain.paragraphs': [2, 3],
|
|
190
|
+
'pain.bearers': [3, 5],
|
|
191
|
+
'pain.figures': [2, 4],
|
|
192
|
+
'pain.causes': [3, 5],
|
|
193
|
+
'context.paragraphs': [1, 2],
|
|
194
|
+
'context.forces': [3, 4],
|
|
195
|
+
'context.evidence': [1, 4],
|
|
196
|
+
'situation.roles': [3, 5],
|
|
197
|
+
'situation.state_of_the_art': [3, 5],
|
|
198
|
+
'trends.items': [3, 5],
|
|
199
|
+
'proposal.paragraphs': [1, 2],
|
|
200
|
+
'proposal.four_cs': [4, 4],
|
|
201
|
+
'proposal.actions': [3, 6],
|
|
202
|
+
'proposal.risks': [1, 4],
|
|
203
|
+
'proposal.unknowns': [1, 4],
|
|
204
|
+
};
|
|
205
|
+
/** The sub-tab (1 to 5) each floor is shown in; 0 is the landing block, shown above the sub-tabs in every state. */
|
|
206
|
+
const floorTab = (key) => ({ headline_figures: 0, pain: 1, context: 2, situation: 3, trends: 4, proposal: 5 })[key.split('.')[0]];
|
|
207
|
+
/** The sub-tabs of the view: the shell's own titles. A copy of EXEC_SECTION_DEFS of src/content/executive.ts; test/executive.test.ts keeps the two equal. */
|
|
185
208
|
export const EXEC_SECTIONS = [
|
|
186
|
-
{ title: { en: '
|
|
187
|
-
{ title: { en: '
|
|
188
|
-
{ title: { en: '
|
|
189
|
-
{ title: { en: '
|
|
209
|
+
{ title: { en: 'The pain', es: 'El problema' } },
|
|
210
|
+
{ title: { en: 'The context', es: 'El contexto' } },
|
|
211
|
+
{ title: { en: 'The current situation', es: 'La situación actual' } },
|
|
212
|
+
{ title: { en: 'The trends', es: 'Las tendencias' } },
|
|
213
|
+
{ title: { en: 'The proposal', es: 'La propuesta' } },
|
|
190
214
|
];
|
|
191
|
-
/**
|
|
215
|
+
/** What the page must never print: the rubric's four labels and four questions, in both languages (Felipe, 2026-10-13: the rubric gives ideas and language, not content). */
|
|
216
|
+
export const EXEC_RUBRIC_LABELS = ['Industry & Exec Tailoring', 'Industria y foco ejecutivo', 'AI Transformation Story', 'Historia de transformación con IA', '4Cs Positioning', 'Posicionamiento 4C', 'Delivery & Credibility', 'Entrega y credibilidad'];
|
|
217
|
+
export const EXEC_RUBRIC_QUESTIONS = ['Built for a C-suite exec?', '¿Pensado para un ejecutivo de primer nivel?', 'Outcomes, not a feature tour', 'Resultados, no un recorrido de funciones', 'Value story, not features dump', 'Historia de valor, no lista de funciones', 'Trust them in front of a CxO?', '¿Confiaría en ellos frente a un ejecutivo?'];
|
|
218
|
+
/** The illustrations and the sub-tab each sits in. */
|
|
192
219
|
export const EXEC_ILLUSTRATIONS = [
|
|
193
|
-
{ name: '
|
|
194
|
-
{ name: '
|
|
195
|
-
{ name: '
|
|
220
|
+
{ name: 'pain', section: 1 },
|
|
221
|
+
{ name: 'situation', section: 3 },
|
|
222
|
+
{ name: 'proposal', section: 5 },
|
|
223
|
+
];
|
|
224
|
+
/** The 4 C's with the names and definitions Felipe gave on 2026-10-13 (the same as FOUR_CS of src/content/executive.ts; test/executive.test.ts keeps them equal). */
|
|
225
|
+
export const EXEC_FOUR_CS = [
|
|
226
|
+
{ name: { en: 'Choice', es: 'Elección' }, definition: { en: 'flexibility across proprietary and open models.', es: 'flexibilidad entre modelos propietarios y abiertos.' } },
|
|
227
|
+
{ name: { en: 'Context', es: 'Contexto' }, definition: { en: 'AI grounded in business meaning, so decisions are accurate.', es: 'IA anclada en el significado del negocio, para que las decisiones sean exactas.' } },
|
|
228
|
+
{ name: { en: 'Cost', es: 'Costo' }, definition: { en: 'visibility of what the AI spends.', es: 'visibilidad de lo que gasta la IA.' } },
|
|
229
|
+
{ name: { en: 'Control', es: 'Control' }, definition: { en: 'one governance over data, models and agents.', es: 'una sola gobernanza sobre datos, modelos y agentes.' } },
|
|
196
230
|
];
|
|
197
|
-
|
|
231
|
+
/** The onward links under the sources (a copy of ONWARD_LINKS of src/content/executive.ts). */
|
|
232
|
+
export const EXEC_LINKS = {
|
|
233
|
+
technical: { en: 'How the work explores this problem', es: 'Cómo explora el trabajo este problema' },
|
|
234
|
+
app: { en: 'Open the App', es: 'Abrir la App' },
|
|
235
|
+
};
|
|
198
236
|
|
|
199
|
-
/** How many sentences a field of the view may hold: one for the single-sentence fields, two for
|
|
237
|
+
/** How many sentences a field of the view may hold: one for the single-sentence fields, two for the key statement, no limit otherwise. */
|
|
200
238
|
export const execSentenceLimit = (path) =>
|
|
201
|
-
/^
|
|
239
|
+
/^title$|\.key_message$|^context\.evidence\[\d+\]\.statement$|^situation\.interaction$|\.signal\.statement$|^proposal\.four_cs\[\d+\]\.applies$|^proposal\.actions\[\d+\]\.(action|check)$|^proposal\.(risks|unknowns)\[\d+\]$|^proposal\.handoff$|^illustrations\.\w+\.caption$/.test(path)
|
|
202
240
|
? 1
|
|
203
|
-
:
|
|
204
|
-
?
|
|
241
|
+
: path === 'key_statement'
|
|
242
|
+
? 2
|
|
205
243
|
: undefined;
|
|
244
|
+
/** The fewest words a field of the view may hold: a paragraph needs EXEC_LIMITS.minParagraphWords. */
|
|
245
|
+
export const execMinWords = (path) => (/\.paragraphs\[\d+\]$/.test(path) ? EXEC_LIMITS.minParagraphWords : undefined);
|
|
206
246
|
|
|
207
247
|
const sentencesOf = (text) =>
|
|
208
248
|
text
|
|
@@ -211,67 +251,200 @@ const sentencesOf = (text) =>
|
|
|
211
251
|
.filter(Boolean);
|
|
212
252
|
const wordsIn = (text) => text.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
|
|
213
253
|
|
|
254
|
+
/** The problems with the title or the note of one source note (the same rules as sourceTextProblems of src/content/executive.ts). */
|
|
255
|
+
export function execSourceTextProblems(text, maxSentences) {
|
|
256
|
+
const out = [];
|
|
257
|
+
const sentences = sentencesOf(text);
|
|
258
|
+
for (const sentence of sentences) {
|
|
259
|
+
const n = wordsIn(sentence);
|
|
260
|
+
if (n > EXEC_LIMITS.maxWords) out.push(`a sentence of ${n} words; the limit is ${EXEC_LIMITS.maxWords}`);
|
|
261
|
+
}
|
|
262
|
+
if (maxSentences !== undefined && sentences.length > maxSentences) out.push(maxSentences === 1 ? `must be one sentence, has ${sentences.length}` : `has ${sentences.length} sentences; the limit is ${maxSentences}`);
|
|
263
|
+
const written = /https?:\/\//i.test(text);
|
|
264
|
+
for (const re of EXEC_LIMITS.fileCitations) {
|
|
265
|
+
if (re.source.includes('pdf') && written) continue;
|
|
266
|
+
const m = re.exec(text);
|
|
267
|
+
if (m) out.push(`cites a file name or number in place of the link ("${m[0]}"); give the publication as the title and its https link as the url`);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
214
272
|
/**
|
|
215
|
-
* The problems of the executive view in one language, from what the page showed
|
|
216
|
-
*
|
|
273
|
+
* The problems of the executive view in one language, from what the page showed in every state of the walk in which
|
|
274
|
+
* the executive view was open (`G.docPage().executive` of gate/inpage.mjs, one entry per state, the first being the
|
|
275
|
+
* state the page opened on). Pure. The five sections are sub-tabs, so one state shows one section: the sections, fields,
|
|
276
|
+
* rows, illustrations and figures of the states are gathered and checked together, and a sub-tab the walk never opened is a
|
|
277
|
+
* failure. An entry is null or {count: 0} when the page renders no ExecutiveView.
|
|
217
278
|
* @returns {string[]} one text per problem
|
|
218
279
|
*/
|
|
219
|
-
export function executiveFailures(
|
|
280
|
+
export function executiveFailures(list, lang) {
|
|
220
281
|
const L = lang.toUpperCase();
|
|
221
282
|
const out = [];
|
|
222
283
|
const bad = (m) => out.push(`Introduction, executive view (${L}): ${m}`);
|
|
223
|
-
|
|
284
|
+
const states = (list ?? []).filter(Boolean);
|
|
285
|
+
const first = states[0];
|
|
286
|
+
if (!first || first.count === 0) {
|
|
224
287
|
bad("no ExecutiveView ([data-executive-view]); the executive view is the shell's ExecutiveView fed by content/executive.json (docs/06_executive-view.md)");
|
|
225
288
|
return out;
|
|
226
289
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
bad(`the
|
|
231
|
-
|
|
290
|
+
for (const facts of states) {
|
|
291
|
+
if (facts.count === 0) continue;
|
|
292
|
+
if (facts.count > 1) bad(`${facts.count} ExecutiveView elements; exactly 1 is required`);
|
|
293
|
+
if (facts.schema !== EXEC_SCHEMA_VERSION) bad(`the view declares schema "${facts.schema ?? 'none'}", expected "${EXEC_SCHEMA_VERSION}"`);
|
|
294
|
+
if (facts.valid !== 'true') {
|
|
295
|
+
bad(`the data file is not valid in the page: ${String(facts.errors || 'no reason given').slice(0, 400)}`);
|
|
296
|
+
return [...new Set(out)];
|
|
297
|
+
}
|
|
298
|
+
if (facts.docSections > 0) bad(`${facts.docSections} DocSection(s) placed in the executive view (${facts.docSectionTitles.join(', ')}); the view has exactly its sections and takes none`);
|
|
299
|
+
if (facts.strangers.length > 0) bad(`${facts.strangers.length} block(s) beside the ExecutiveView (${facts.strangers.slice(0, 3).map((x) => `"${x}"`).join(', ')}); the view is the whole of the executive view`);
|
|
300
|
+
if (!facts.inOpenView) bad('the ExecutiveView is not in the open executive view');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// the landing block above the sub-tabs: the title, the key statement, two or three headline figures
|
|
304
|
+
const landing = first.landing;
|
|
305
|
+
if (!landing || !landing.present) bad('no landing block above the sub-tabs (the title, the key statement and the headline figures)');
|
|
306
|
+
else {
|
|
307
|
+
if (!landing.title) bad('the landing block has no title');
|
|
308
|
+
if (!landing.statement) bad('the landing block has no key statement');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// the sections are the sub-tabs, in order, the first selected, one section shown at a time
|
|
312
|
+
const tabs = first.tabs ?? [];
|
|
313
|
+
if (tabs.length === 0) bad('the sections are not sub-tabs: the view has no sub-tab list; the sections must be the sub-tabs of SubTabs, one per section (shell 0.13.0)');
|
|
314
|
+
else {
|
|
315
|
+
if (tabs.length !== EXEC_SECTIONS.length) bad(`the executive view shows ${tabs.length} sub-tab(s), exactly ${EXEC_SECTIONS.length} are required (one per section)`);
|
|
316
|
+
EXEC_SECTIONS.forEach((def, i) => {
|
|
317
|
+
if (tabs[i] !== undefined && tabs[i] !== def.title[lang]) bad(`sub-tab ${i + 1} is labelled "${tabs[i]}", expected "${def.title[lang]}"`);
|
|
318
|
+
});
|
|
319
|
+
if (first.activeTab !== 0) bad('the first sub-tab is not the one selected when the view opens');
|
|
320
|
+
}
|
|
321
|
+
for (const facts of states) {
|
|
322
|
+
if (facts.count === 0) continue;
|
|
323
|
+
if (facts.sections.length > 1) bad(`${facts.sections.length} sections shown at once; the view shows one section at a time, each in its own sub-tab`);
|
|
324
|
+
}
|
|
325
|
+
const bySection = new Map();
|
|
326
|
+
for (const facts of states) {
|
|
327
|
+
if (facts.count === 0) continue;
|
|
328
|
+
for (const sec of facts.sections) {
|
|
329
|
+
if (!bySection.has(sec.n)) bySection.set(sec.n, { sec, facts });
|
|
330
|
+
if (tabs.length > 0 && facts.sections.length === 1 && facts.activeTab !== sec.n - 1) bad(`section ${sec.n} sits in sub-tab ${facts.activeTab + 1}, it belongs in sub-tab ${sec.n}`);
|
|
331
|
+
}
|
|
232
332
|
}
|
|
233
|
-
if (facts.docSections > 0) bad(`${facts.docSections} DocSection(s) placed in the executive view (${facts.docSectionTitles.join(', ')}); the view has exactly its four sections and takes none`);
|
|
234
|
-
if (facts.strangers.length > 0) bad(`${facts.strangers.length} block(s) beside the ExecutiveView (${facts.strangers.slice(0, 3).map((x) => `"${x}"`).join(', ')}); the view is the whole of the executive view`);
|
|
235
|
-
if (!facts.inOpenView) bad('the ExecutiveView is not in the open executive view');
|
|
236
|
-
// the four sections, in order, with their titles and questions
|
|
237
|
-
if (facts.sections.length !== 4) bad(`${facts.sections.length} section(s), exactly 4 are required`);
|
|
238
333
|
EXEC_SECTIONS.forEach((def, i) => {
|
|
239
|
-
const got =
|
|
240
|
-
if (!got)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
334
|
+
const got = bySection.get(i + 1);
|
|
335
|
+
if (!got) {
|
|
336
|
+
bad(`sub-tab "${def.title[lang]}" was never visited, so its section was never checked`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (got.sec.title !== def.title[lang]) bad(`section ${i + 1} is titled "${got.sec.title}", expected "${def.title[lang]}"`);
|
|
340
|
+
// every sub-tab opens with its key message line, which a reader can read on its own
|
|
341
|
+
if (!got.sec.keyFirst) bad(`section ${i + 1} ("${def.title[lang]}") does not open with its key message line`);
|
|
342
|
+
else if (!got.sec.keyMessage) bad(`section ${i + 1} ("${def.title[lang]}") has an empty key message line`);
|
|
244
343
|
});
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
344
|
+
for (const n of bySection.keys()) if (n < 1 || n > EXEC_SECTIONS.length) bad(`section ${n} is not one of the ${EXEC_SECTIONS.length} sections`);
|
|
345
|
+
|
|
346
|
+
// the floors: the rows each part of the view shows, at least the minimum and at most the maximum
|
|
347
|
+
const visited = new Set([0, ...bySection.keys()]);
|
|
348
|
+
for (const [key, [min, max]] of Object.entries(EXEC_FLOORS)) {
|
|
349
|
+
if (!visited.has(floorTab(key))) continue;
|
|
350
|
+
const n = Math.max(0, ...states.map((x) => x.items?.[key] ?? 0));
|
|
351
|
+
if (n < min) bad(`${key}: ${n} row(s) are shown, at least ${min} are required`);
|
|
352
|
+
else if (n > max) bad(`${key}: ${n} row(s) are shown, at most ${max} are allowed`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// the 4 C's of the proposal: Felipe's names and definitions, in order, each applied to the proposal
|
|
356
|
+
const s5 = bySection.get(5);
|
|
357
|
+
if (s5) {
|
|
358
|
+
const cs = s5.facts.cs ?? [];
|
|
359
|
+
if (cs.length !== 4) bad(`the proposal holds ${cs.length} row(s) of the 4 C's, exactly 4 are required`);
|
|
360
|
+
EXEC_FOUR_CS.forEach((c, i) => {
|
|
361
|
+
const got = cs[i];
|
|
362
|
+
if (!got) return;
|
|
363
|
+
if (got.name !== c.name[lang]) bad(`C ${i + 1} is named "${got.name}", expected "${c.name[lang]}" (the 4 C's in Felipe's order: ${EXEC_FOUR_CS.map((x) => x.name[lang]).join(', ')})`);
|
|
364
|
+
else if (got.def !== c.definition[lang]) bad(`C ${i + 1} ("${c.name[lang]}") defines itself as "${got.def}", expected "${c.definition[lang]}"`);
|
|
365
|
+
else if (!got.applies) bad(`C ${i + 1} ("${c.name[lang]}") says nothing about what it means for the proposal`);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// the view takes the full width of the document column (its illustrations may stay centred inside their figure)
|
|
370
|
+
const narrow = new Set();
|
|
371
|
+
let column = 0;
|
|
372
|
+
for (const facts of states) {
|
|
373
|
+
column = Math.max(column, facts.column ?? 0);
|
|
374
|
+
for (const x of facts.narrow ?? []) narrow.add(x);
|
|
375
|
+
}
|
|
376
|
+
if (narrow.size > 0) bad(`${narrow.size} block(s) narrower than the ${column}px document column: ${[...narrow].slice(0, 4).join(', ')}; the executive view takes the full width of the column`);
|
|
377
|
+
|
|
378
|
+
// the rubric's labels and questions are never printed on the page
|
|
379
|
+
const printed = new Map();
|
|
380
|
+
for (const facts of states) {
|
|
381
|
+
const text = String(facts.text ?? '').toLowerCase();
|
|
382
|
+
for (const [kind, names] of [['label', EXEC_RUBRIC_LABELS], ['question', EXEC_RUBRIC_QUESTIONS]]) for (const x of names) if (text.includes(x.toLowerCase())) printed.set(x, kind);
|
|
251
383
|
}
|
|
252
|
-
|
|
384
|
+
for (const [x, kind] of printed) bad(`prints the rubric's ${kind} "${x}"; the rubric gives the ideas and the language, not the content (docs/06_executive-view.md)`);
|
|
385
|
+
|
|
386
|
+
// the writing limits, on the text a reader sees, across every sub-tab
|
|
387
|
+
const fields = new Map();
|
|
388
|
+
for (const facts of states) for (const f of facts.fields ?? []) fields.set(`${f.path}|${f.text}`, f);
|
|
253
389
|
const unreadable = [];
|
|
254
|
-
for (const f of
|
|
390
|
+
for (const f of fields.values()) {
|
|
255
391
|
if (!f.readable) {
|
|
256
392
|
unreadable.push(f.path);
|
|
257
393
|
continue;
|
|
258
394
|
}
|
|
259
395
|
const sentences = sentencesOf(f.text);
|
|
260
|
-
for (const
|
|
261
|
-
const n = wordsIn(
|
|
396
|
+
for (const sentence of sentences) {
|
|
397
|
+
const n = wordsIn(sentence);
|
|
262
398
|
if (n > EXEC_LIMITS.maxWords) bad(`field "${f.path}": a sentence of ${n} words; the limit is ${EXEC_LIMITS.maxWords}`);
|
|
263
399
|
}
|
|
264
400
|
const max = execSentenceLimit(f.path);
|
|
265
401
|
if (max !== undefined && sentences.length > max) bad(`field "${f.path}": ${sentences.length} sentences; the limit is ${max}`);
|
|
402
|
+
const min = execMinWords(f.path);
|
|
403
|
+
if (min !== undefined && wordsIn(f.text) < min) bad(`field "${f.path}": ${wordsIn(f.text)} words; a paragraph needs at least ${min}`);
|
|
266
404
|
if (EXEC_LIMITS.equation.test(f.text)) bad(`field "${f.path}": holds an equation or a TeX marker`);
|
|
267
405
|
if (EXEC_LIMITS.citations.some((re) => re.test(f.text))) bad(`field "${f.path}": holds an inline citation (sources are numbered notes)`);
|
|
406
|
+
for (const re of EXEC_LIMITS.promotional) {
|
|
407
|
+
const m = re.exec(f.text);
|
|
408
|
+
if (m) bad(`field "${f.path}": holds promotional or client-bound wording ("${m[0]}"); the executive view is general, never a client, never a pitch`);
|
|
409
|
+
}
|
|
268
410
|
}
|
|
269
|
-
if (unreadable.length > 0) bad(`${unreadable.length} field(s) are in the page and a reader cannot read them: ${unreadable.slice(0, 4).join(', ')}`);
|
|
270
|
-
|
|
411
|
+
if (unreadable.length > 0) bad(`${unreadable.length} field(s) are in the page and a reader cannot read them: ${[...new Set(unreadable)].slice(0, 4).join(', ')}`);
|
|
412
|
+
|
|
413
|
+
// the source notes: always there, one per source cited, each a title that links to an https url, with a note that cites no file
|
|
414
|
+
const cited = new Set(states.flatMap((x) => x.cites ?? []));
|
|
415
|
+
for (const facts of states) {
|
|
416
|
+
const notes = facts.notes ?? [];
|
|
417
|
+
if (notes.length !== cited.size) bad(`${notes.length} source note(s) for ${cited.size} source(s) cited`);
|
|
418
|
+
notes.forEach((n, i) => {
|
|
419
|
+
const at = `source note ${i + 1}`;
|
|
420
|
+
if (!n.href || !n.title) {
|
|
421
|
+
bad(`${at} has no link: a source is its title linked to an https:// url (docs/06_executive-view.md)`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!/^https:\/\/\S+$/.test(n.href)) bad(`${at} links to "${n.href}"; a source is an https:// link`);
|
|
425
|
+
for (const [what, text, max] of [['title', n.title, undefined], ['note', n.note, 1]]) {
|
|
426
|
+
if (!text) continue;
|
|
427
|
+
for (const m of execSourceTextProblems(text, max)) bad(`${at}: the ${what} ${m}`);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// the two onward links under the sources, worded as destinations
|
|
433
|
+
const links = first.links ?? [];
|
|
434
|
+
for (const kind of ['technical', 'app']) {
|
|
435
|
+
const got = links.find((x) => x.kind === kind);
|
|
436
|
+
if (!got) bad(`no onward link to ${kind === 'app' ? 'the App' : 'the technical view'} under the sources`);
|
|
437
|
+
else if (got.text !== EXEC_LINKS[kind][lang]) bad(`the onward link to ${kind === 'app' ? 'the App' : 'the technical view'} reads "${got.text}", expected "${EXEC_LINKS[kind][lang]}"`);
|
|
438
|
+
else if (kind === 'app' && !/\/app\/?(\?|#|$)/.test(got.href ?? '')) bad(`the onward link to the App goes to "${got.href}", not to the App route`);
|
|
439
|
+
}
|
|
440
|
+
|
|
271
441
|
// the three illustrations, in their places
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
442
|
+
const seen = new Map();
|
|
443
|
+
for (const facts of states) for (const ill of facts.illustrations ?? []) if (!seen.has(ill.name)) seen.set(ill.name, ill);
|
|
444
|
+
const names = [...seen.keys()];
|
|
445
|
+
const expected = EXEC_ILLUSTRATIONS.map((x) => x.name);
|
|
446
|
+
if (names.slice().sort().join() !== expected.slice().sort().join()) bad(`the illustrations are [${names.join(', ')}], expected [${expected.join(', ')}]`);
|
|
447
|
+
for (const ill of seen.values()) {
|
|
275
448
|
const want = EXEC_ILLUSTRATIONS.find((x) => x.name === ill.name);
|
|
276
449
|
const name = `illustration "${ill.name}"`;
|
|
277
450
|
if (want && ill.section !== want.section) bad(`${name} sits in section ${ill.section}, it belongs in section ${want.section}`);
|
|
@@ -369,6 +542,7 @@ export function docFloorFailures({ route, lang, states, expectedTopics }) {
|
|
|
369
542
|
const figures = new Set();
|
|
370
543
|
const equations = new Set();
|
|
371
544
|
const execMessages = new Set();
|
|
545
|
+
const execStates = [];
|
|
372
546
|
for (const st of states) {
|
|
373
547
|
for (const t of st.topics) topics.set(t.key, t.label);
|
|
374
548
|
const ordinal = new Map();
|
|
@@ -386,11 +560,12 @@ export function docFloorFailures({ route, lang, states, expectedTopics }) {
|
|
|
386
560
|
bad('', `${NAME} (${L}): the ExecutiveView sits in the "${st.view ?? 'none'}" view; it belongs to the executive view`);
|
|
387
561
|
}
|
|
388
562
|
if (route === '/introduction' && st.view === 'executive') {
|
|
389
|
-
|
|
563
|
+
execStates.push(st.executive);
|
|
390
564
|
if (st.viewEquations > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewEquations} equation(s); the executive view holds no equation`);
|
|
391
565
|
if (st.viewGlossaries > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewGlossaries} symbol glossary(ies); the executive view holds none`);
|
|
392
566
|
}
|
|
393
567
|
}
|
|
568
|
+
if (route === '/introduction' && execStates.length > 0) for (const m of executiveFailures(execStates, lang)) execMessages.add(m);
|
|
394
569
|
for (const m of execMessages) bad('', m);
|
|
395
570
|
const seen = [...sections.values()].map(mergeSections);
|
|
396
571
|
const tname = (label) => (label ? `topic "${label}"` : 'the page');
|
|
@@ -398,6 +573,7 @@ export function docFloorFailures({ route, lang, states, expectedTopics }) {
|
|
|
398
573
|
if (expectedTopics > topics.size) bad('', `${NAME} (${L}): the walk visited ${topics.size} of ${expectedTopics} topics; the floors of an unvisited topic were never checked`);
|
|
399
574
|
|
|
400
575
|
// ---- the lede
|
|
576
|
+
if (first.ledeWidth > 0 && first.ledeWidth < first.columnWidth - 2) bad('', `${NAME} (${L}): the lede is ${first.ledeWidth}px wide in a ${first.columnWidth}px document column; the lede takes the full width of the column`);
|
|
401
577
|
if (!waived.has('lede-words') && first.ledeWords < LEDE_WORDS) bad('', `${NAME} (${L}): the lede holds ${first.ledeWords} word(s), the floor is ${LEDE_WORDS}`);
|
|
402
578
|
|
|
403
579
|
// ---- the Introduction's views: the executive one first
|
package/gate/inpage.mjs
CHANGED
|
@@ -503,6 +503,9 @@ export function installLib() {
|
|
|
503
503
|
docBound = page;
|
|
504
504
|
const seen = (el) => readable(el, page, 4, 4);
|
|
505
505
|
const lede = page.querySelector('.lede');
|
|
506
|
+
// the document column: the page body without its padding; the lede and the executive view take all of it (Felipe, 2026-10-13)
|
|
507
|
+
const pageStyle = getComputedStyle(page);
|
|
508
|
+
const columnWidth = Math.round(page.clientWidth - parseFloat(pageStyle.paddingLeft) - parseFloat(pageStyle.paddingRight));
|
|
506
509
|
const panels = [...page.querySelectorAll('[role="tabpanel"]')].filter(visible);
|
|
507
510
|
// a tab list inside a section belongs to that section: its panels are not topics, and it does not make its topic a branch
|
|
508
511
|
const inSection = (el) => Boolean(el.closest('[data-doc-section]'));
|
|
@@ -614,7 +617,7 @@ export function installLib() {
|
|
|
614
617
|
};
|
|
615
618
|
});
|
|
616
619
|
const selected = outerTabs.find((t) => t.getAttribute('aria-selected') === 'true');
|
|
617
|
-
// The executive view (shell 0.
|
|
620
|
+
// The executive view (shell 0.13.0): what the ExecutiveView shows in this state (one sub-tab = one section), for executiveFailures of gate/areas.mjs.
|
|
618
621
|
const execRoots = [...page.querySelectorAll('[data-executive-view]')].filter(visible);
|
|
619
622
|
const executive = (() => {
|
|
620
623
|
if (!execRoots.length) return { count: 0 };
|
|
@@ -660,17 +663,31 @@ export function installLib() {
|
|
|
660
663
|
return info;
|
|
661
664
|
});
|
|
662
665
|
const sections = [...root.querySelectorAll('[data-exec-section]')].map((sec) => {
|
|
663
|
-
const
|
|
666
|
+
const key = sec.querySelector('[data-exec-key-message]');
|
|
664
667
|
return {
|
|
665
668
|
n: Number(sec.getAttribute('data-exec-section')),
|
|
666
|
-
title:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
pendingState: pending ? pending.getAttribute('data-state') : null,
|
|
671
|
-
rows: [...sec.querySelectorAll('[data-exec-c]')].filter(seen).length,
|
|
669
|
+
title: (sec.getAttribute('aria-label') || '').trim(),
|
|
670
|
+
// the key message line comes first in the section, and a reader can read it
|
|
671
|
+
keyFirst: Boolean(key) && sec.firstElementChild === key,
|
|
672
|
+
keyMessage: key && seen(key) ? textOf(key) : '',
|
|
672
673
|
};
|
|
673
674
|
});
|
|
675
|
+
// the rows each part of the view shows in this state (the floors), counted once per kind
|
|
676
|
+
const items = {};
|
|
677
|
+
for (const el of root.querySelectorAll('[data-exec-item]')) if (seen(el)) items[el.getAttribute('data-exec-item')] = (items[el.getAttribute('data-exec-item')] ?? 0) + 1;
|
|
678
|
+
const landingEl = root.querySelector('[data-exec-landing]');
|
|
679
|
+
const landing = {
|
|
680
|
+
present: Boolean(landingEl && seen(landingEl)),
|
|
681
|
+
title: landingEl ? textOf(landingEl.querySelector('[data-exec-field="title"]') || landingEl).slice(0, 80) : '',
|
|
682
|
+
statement: landingEl ? textOf(landingEl.querySelector('[data-exec-field="key_statement"]') || landingEl).slice(0, 80) : '',
|
|
683
|
+
};
|
|
684
|
+
// the 4 C's a reader sees in the proposal: the name, the definition and what each means for the proposal
|
|
685
|
+
const cs = [...root.querySelectorAll('[data-exec-c]')]
|
|
686
|
+
.filter(seen)
|
|
687
|
+
.map((li) => ({ name: textOf(li.querySelector('[data-exec-cname]') || li), def: textOf(li.querySelector('[data-exec-cdef]') || li), applies: textOf(li.querySelector('[data-exec-field$=".applies"]') || li.querySelector('span') || li) }));
|
|
688
|
+
const links = [...root.querySelectorAll('[data-exec-links] a')].map((a) => ({ kind: a.getAttribute('data-exec-link'), text: textOf(a), href: a.getAttribute('href') }));
|
|
689
|
+
// the sub-tabs of the view: their labels (all of them, shown or not), and the one selected
|
|
690
|
+
const tabEls = [...root.querySelectorAll('[data-exec-tabs] [role="tab"]')];
|
|
674
691
|
const panelOpen = outerPanel ? outerPanel.contains(root) : true;
|
|
675
692
|
const execOpen = selected && (selected.getAttribute('data-tab') || tabLabel(selected)) === 'executive';
|
|
676
693
|
const area = execOpen && outerPanel ? outerPanel : null;
|
|
@@ -691,15 +708,40 @@ export function installLib() {
|
|
|
691
708
|
docSectionTitles: docSections.map((e) => textOf(e.querySelector('h2') || e).slice(0, 40)),
|
|
692
709
|
strangers,
|
|
693
710
|
sections,
|
|
711
|
+
landing,
|
|
712
|
+
items,
|
|
713
|
+
cs,
|
|
714
|
+
links,
|
|
694
715
|
fields: [...root.querySelectorAll('[data-exec-field]')].map((e) => ({ path: e.getAttribute('data-exec-field'), text: textOf(e), readable: seen(e) })),
|
|
695
|
-
|
|
696
|
-
|
|
716
|
+
// the blocks of this state narrower than the document column (an illustration may stay centred inside its figure)
|
|
717
|
+
narrow: [...root.querySelectorAll('.subtabs, .caos-exec-notes-wrap, [data-exec-section], [data-exec-section] > *')]
|
|
718
|
+
.filter((e) => visible(e))
|
|
719
|
+
.map((e) => [`${e.tagName.toLowerCase()}${e.className && typeof e.className === 'string' ? '.' + e.className.split(' ')[0] : ''}`, Math.round(e.getBoundingClientRect().width)])
|
|
720
|
+
.filter(([, w]) => w < columnWidth - 2)
|
|
721
|
+
.map(([name, w]) => `${name} ${w}px`),
|
|
722
|
+
column: columnWidth,
|
|
723
|
+
// everything the view prints in this state, for the check that no rubric label or question is on the page
|
|
724
|
+
text: textOf(root),
|
|
725
|
+
tabs: tabEls.map(tabLabel),
|
|
726
|
+
activeTab: tabEls.findIndex((t) => t.getAttribute('aria-selected') === 'true'),
|
|
727
|
+
// each source note: the link it holds (its target and its text) and the text beside the link
|
|
728
|
+
notes: [...root.querySelectorAll('[data-exec-notes] > li')].map((li) => {
|
|
729
|
+
const a = li.querySelector('a');
|
|
730
|
+
const link = a && seen(a) ? textOf(a) : '';
|
|
731
|
+
const note = li.querySelector('[data-exec-source-note]');
|
|
732
|
+
const rest = note ? textOf(note) : textOf(li).replace(link, '').trim();
|
|
733
|
+
return { href: a ? a.getAttribute('href') : null, title: link, note: rest };
|
|
734
|
+
}),
|
|
735
|
+
// the figures this state shows: the notes they point at (the same figure seen in two states counts once)
|
|
736
|
+
cites: [...root.querySelectorAll('.caos-exec-cite a')].map((a) => a.getAttribute('href')),
|
|
697
737
|
illustrations,
|
|
698
738
|
};
|
|
699
739
|
})();
|
|
700
740
|
const inView = (sel) => (outerPanel ? [...outerPanel.querySelectorAll(sel)] : [...page.querySelectorAll(sel)]).filter(seen);
|
|
701
741
|
return {
|
|
702
742
|
ledeWords: lede && visible(lede) ? wordsOf(lede).words : 0,
|
|
743
|
+
ledeWidth: lede && visible(lede) ? Math.round(lede.getBoundingClientRect().width) : 0,
|
|
744
|
+
columnWidth,
|
|
703
745
|
waiver: page.getAttribute('data-floor-waiver'),
|
|
704
746
|
topics,
|
|
705
747
|
views: outerTabs.map((t) => t.getAttribute('data-tab') || tabLabel(t)),
|
package/gate/run.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export const DEFAULTS = {
|
|
|
37
37
|
sizes: '390x844,768x1024,1280x800,1600x900,2560x1440',
|
|
38
38
|
themes: 'light,dark',
|
|
39
39
|
langs: 'en,es',
|
|
40
|
-
workbench: '/',
|
|
40
|
+
workbench: '/app',
|
|
41
41
|
idleMs: 3000,
|
|
42
42
|
settleMs: 15000,
|
|
43
43
|
textFloor: 400,
|
|
@@ -84,6 +84,7 @@ export async function runGate(o) {
|
|
|
84
84
|
const sizes = String(opt.sizes).split(',').map((s) => s.trim().split('x').map(Number));
|
|
85
85
|
const themes = String(opt.themes).split(',').map((s) => s.trim());
|
|
86
86
|
const langs = String(opt.langs).split(',').map((s) => s.trim());
|
|
87
|
+
const workbenchRoute = String(opt.workbench).split(',')[0].trim() || '/app';
|
|
87
88
|
const workbenchRoutes = new Set(String(opt.workbench).split(',').map((s) => s.trim()).filter(Boolean));
|
|
88
89
|
const primarySize = sizes.find(([w]) => w >= opt.viewportMinWidth) ?? sizes[0];
|
|
89
90
|
const shotsDir = join(opt.out, 'shots');
|
|
@@ -240,15 +241,39 @@ export async function runGate(o) {
|
|
|
240
241
|
await ctx.close();
|
|
241
242
|
return finish();
|
|
242
243
|
}
|
|
243
|
-
if (why) fail('G7', { route: '/' }, `the
|
|
244
|
+
if (why) fail('G7', { route: '/' }, `the landing route never settled: ${why}`);
|
|
244
245
|
if (!routes) {
|
|
245
246
|
const hrefs = await page.$$eval('header nav a[href]', (as) => as.map((a) => a.href));
|
|
246
|
-
routes = [...new Set(
|
|
247
|
+
routes = [...new Set(hrefs.map(routeOf).filter(Boolean))];
|
|
247
248
|
}
|
|
248
249
|
sink.splice(0);
|
|
249
250
|
await ctx.close();
|
|
250
251
|
}
|
|
251
252
|
|
|
253
|
+
// ---- The landing (shell 0.13.0): the root opens the Introduction on its executive view; an old App link with a query string reaches the App.
|
|
254
|
+
{
|
|
255
|
+
const ctx = await openContext(primarySize, themes[0], langs[0]);
|
|
256
|
+
const { page, sink } = await newPage(ctx);
|
|
257
|
+
await page.goto(urlFor('/'), { waitUntil: 'load', timeout: 30000 }).catch(() => undefined);
|
|
258
|
+
await settle(page);
|
|
259
|
+
const at = { route: '/', mode: 'landing' };
|
|
260
|
+
if (routeOf(page.url()) !== '/introduction') {
|
|
261
|
+
fail('G4', at, `the root opened ${routeOf(page.url()) ?? page.url()}, not the Introduction; every product lands on the Introduction (Landing of the shell on the root path)`);
|
|
262
|
+
} else {
|
|
263
|
+
const landed = await page.evaluate(() => window.__caosGate.docPage());
|
|
264
|
+
if (!landed || landed.view !== 'executive' || !landed.executive || landed.executive.count !== 1) fail('G4', at, `the root opened the Introduction on the "${landed?.view ?? 'none'}" view; the landing opens its executive view`);
|
|
265
|
+
}
|
|
266
|
+
const old = await page.goto(`${urlFor('/')}?gate=landing#frag`, { waitUntil: 'load', timeout: 30000 }).catch(() => undefined);
|
|
267
|
+
await settle(page);
|
|
268
|
+
const u = new URL(page.url());
|
|
269
|
+
if (routeOf(page.url()) !== workbenchRoute || u.searchParams.get('gate') !== 'landing' || u.hash !== '#frag') {
|
|
270
|
+
fail('G4', { route: '/', mode: 'landing' }, `an old App link /?gate=landing#frag reached ${routeOf(page.url())}${u.search}${u.hash}; it must reach ${workbenchRoute} with its query string and hash kept (Landing keeps the query string and the hash)`);
|
|
271
|
+
}
|
|
272
|
+
void old;
|
|
273
|
+
sink.splice(0);
|
|
274
|
+
await ctx.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
252
277
|
// ---- The walk: every mode, every route, every tab, every case (G2 to G9 and the ADR measures).
|
|
253
278
|
for (const size of sizes) {
|
|
254
279
|
for (const theme of themes) {
|
|
@@ -281,14 +306,14 @@ export async function runGate(o) {
|
|
|
281
306
|
}
|
|
282
307
|
if (id.theme !== theme) fail('G2', at, `asked for the ${theme} theme through ${THEME_STORAGE_KEY}; the document has data-theme=${id.theme}`);
|
|
283
308
|
if (!String(id.lang ?? '').startsWith(lang)) fail('G2', at, `asked for ${lang} through ${LANG_STORAGE_KEY}; <html lang> is ${id.lang}`);
|
|
284
|
-
if (
|
|
309
|
+
if (id.activeHref && routeOf(id.activeHref) !== route) fail('G4', at, `loaded directly, the page shows route ${routeOf(id.activeHref)}`);
|
|
285
310
|
|
|
286
311
|
// ADR-0017 2.1: the documentation floors are measured once per language, at the primary size and first theme
|
|
287
312
|
const docFloors = size === primarySize && theme === themes[0] && DOC_ROUTES.includes(route);
|
|
288
313
|
const walkState = { tested: new Set(), keyedViews: 0, controls: 0, tablists: 0, doc: docFloors ? [] : null };
|
|
289
314
|
await processState(page, sink, { ...at, trail: '' }, { route, isWorkbench, primary, idle: idleHere, first: true, size, w, h, theme, lang, walkState, contrast });
|
|
290
315
|
|
|
291
|
-
if (primary
|
|
316
|
+
if (primary) {
|
|
292
317
|
// G4: the trailing-slash form a static host serves for a materialised route.
|
|
293
318
|
const slash = `${urlFor(route)}/`;
|
|
294
319
|
const r2 = await page.goto(slash, { waitUntil: 'load', timeout: 30000 }).catch((e) => e);
|
|
@@ -324,8 +349,8 @@ export async function runGate(o) {
|
|
|
324
349
|
|
|
325
350
|
if (primary) {
|
|
326
351
|
// ADR-0071 rule 9 / G5: every header route reached by a real pointer click from the App route.
|
|
327
|
-
for (const route of routes.filter((r) => r !==
|
|
328
|
-
await page.goto(urlFor(
|
|
352
|
+
for (const route of routes.filter((r) => r !== workbenchRoute)) {
|
|
353
|
+
await page.goto(urlFor(workbenchRoute), { waitUntil: 'load', timeout: 30000 }).catch(() => undefined);
|
|
329
354
|
await settle(page);
|
|
330
355
|
const link = page.locator('header nav a[href]');
|
|
331
356
|
const hrefs = await link.evaluateAll((as) => as.map((a) => a.href));
|
|
@@ -751,5 +776,5 @@ export function captureIndex(report, captures) {
|
|
|
751
776
|
}
|
|
752
777
|
|
|
753
778
|
function slug(route) {
|
|
754
|
-
return route === '/' ? '
|
|
779
|
+
return route === '/' ? 'landing' : route.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '');
|
|
755
780
|
}
|