@fasl-work/caos-app-shell 0.11.2 → 0.13.0
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 +72 -72
- package/dist/chart.js +2 -2
- package/dist/{chunk-DX36XRD5.js → chunk-3QYQCWSZ.js} +1 -1
- package/dist/{chunk-X6Y5CDUD.js → chunk-YVVDBSAC.js} +5 -3
- package/dist/index.d.ts +187 -3
- package/dist/index.js +1349 -43
- package/dist/{keys-CnvqkckF.d.ts → keys-CTQZafOf.d.ts} +7 -3
- package/dist/keys.d.ts +1 -1
- package/dist/keys.js +1 -1
- package/gate/areas.mjs +631 -327
- package/gate/inpage.mjs +1270 -1142
- package/gate/run.mjs +780 -755
- package/gate/serve.mjs +101 -97
- package/package.json +4 -2
- package/reserved-classes.json +86 -0
- package/src/content/executive.schema.json +659 -0
- package/styles.css +769 -717
package/gate/areas.mjs
CHANGED
|
@@ -1,327 +1,631 @@
|
|
|
1
|
-
// The required areas of the documentation pages (ADR-0017 section 2.1), in ONE table. The gate's page library
|
|
2
|
-
// (inpage.mjs) measures what a section holds; this module says what each area must hold, and run.mjs applies it.
|
|
3
|
-
// docs/04_views.md transcribes the table and a unit test keeps the two in step: a rule is never written twice.
|
|
4
|
-
//
|
|
5
|
-
// A "topic" is a leaf panel of a page (a tab, a sub-tab or a tab-group item) that holds sections; a page without tabs is
|
|
6
|
-
// one topic. A tab list inside a section belongs to that section: its panels are not topics, and what they hold counts
|
|
7
|
-
// for the section across every state the gate visited.
|
|
8
|
-
// A "dense paragraph" is a visible `<p>` of at least DENSE_WORDS words in the displayed language, equations removed,
|
|
9
|
-
// counted once per distinct text. Hidden elements never count.
|
|
10
|
-
|
|
11
|
-
export const DENSE_WORDS = 40;
|
|
12
|
-
export const LEDE_WORDS = 35;
|
|
13
|
-
export const REASON_MIN = 12;
|
|
14
|
-
export const METHOD_KINDS = ['classical', 'learned'];
|
|
15
|
-
|
|
16
|
-
/** What a rule counts in a section: the key of the measurement taken by `G.docSections` and how it reads in a message. */
|
|
17
|
-
export const RULES = {
|
|
18
|
-
dense: { noun: 'dense paragraph(s)', min: true },
|
|
19
|
-
equations: { noun: 'captioned equation(s)', min: true },
|
|
20
|
-
figures: { noun: 'figure(s) with a caption', min: true },
|
|
21
|
-
callouts: { noun: 'callout(s)', min: true },
|
|
22
|
-
glossary: { noun: 'glossary item(s)', min: true },
|
|
23
|
-
steps: { noun: 'step(s) in an ordered list', min: true },
|
|
24
|
-
controls: { noun: 'control(s)', min: true },
|
|
25
|
-
refs: { noun: 'reference(s) cited', min: true },
|
|
26
|
-
stats: { noun: 'quantified statement(s) (value, unit, source)', min: true },
|
|
27
|
-
rows: { noun: 'row(s) of capability, outcome and proof point', min: true },
|
|
28
|
-
rowsNoStat: { noun: 'row(s) whose proof point is not a quantified statement', max: true },
|
|
29
|
-
items: { noun: 'item(s)', exact: true },
|
|
30
|
-
thinItems: { noun: 'item(s) without a sentence', max: true },
|
|
31
|
-
qa: { noun: 'question and answer pair(s)', min: true },
|
|
32
|
-
honest: { noun: 'honest callout(s)', exact: true },
|
|
33
|
-
honestOrStrong: { noun: 'honest or strong callout(s)', exact: true },
|
|
34
|
-
// derived: tables and charts are alternatives in some areas
|
|
35
|
-
table: { noun: 'table(s)', min: true },
|
|
36
|
-
chart: { noun: 'chart(s)', min: true },
|
|
37
|
-
tableOrChart: { noun: 'table(s) or chart(s)', min: true, of: ['tables', 'charts'] },
|
|
38
|
-
tableOrCode: { noun: 'table(s) or code block(s)', min: true, of: ['tables', 'codes'] },
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const measureOf = (m, key) => {
|
|
42
|
-
const r = RULES[key];
|
|
43
|
-
if (r.of) return r.of.reduce((a, k) => a + (m[k] ?? 0), 0);
|
|
44
|
-
if (key === 'table') return m.tables ?? 0;
|
|
45
|
-
if (key === 'chart') return m.charts ?? 0;
|
|
46
|
-
if (key === 'honestOrStrong') return (m.honest ?? 0) + (m.strong ?? 0);
|
|
47
|
-
return m[key] ?? 0;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* The floors a section fails. `m` is the measurement of the section (see `G.docSections`), `rule` the area's rule
|
|
52
|
-
* (`{ dense: 2, equations: 1, honest: 1 }`: a number is a minimum, or the exact count for `honest` and `honestOrStrong`).
|
|
53
|
-
* @returns {string[]} one text per failed rule, with the measured number and the floor
|
|
54
|
-
*/
|
|
55
|
-
export function failedRules(m, rule) {
|
|
56
|
-
const out = [];
|
|
57
|
-
for (const [key, want] of Object.entries(rule)) {
|
|
58
|
-
const def = RULES[key];
|
|
59
|
-
if (!def) throw new Error(`gate/areas.mjs: unknown rule ${key}`);
|
|
60
|
-
const n = measureOf(m, key);
|
|
61
|
-
if (def.max ? n > want : def.exact ? n !== want : n < want) out.push(`${n} ${plural(def.noun, n)}, ${def.max ? `at most ${want} is allowed` : def.exact ? `exactly ${want} is required` : `the floor is ${want}`}`);
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The rule as a sentence, for the docs and the gate's messages. */
|
|
67
|
-
export function describeRule(rule) {
|
|
68
|
-
return Object.entries(rule)
|
|
69
|
-
.map(([key, want]) => `${RULES[key].max ? 'at most' : RULES[key].exact ? 'exactly' : 'at least'} ${want} ${RULES[key].noun.replace('(s)', want === 1 ? '' : 's')}`)
|
|
70
|
-
.join(', ');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const A = (en, es, requires, extra = {}) => ({ what: { en, es }, requires, ...extra });
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The five tables of ADR-0017 section 2.1. `areas`: the vocabulary of the page (name, what it is in both languages,
|
|
77
|
-
* what the gate requires in a section of that area). `floors`: the page-level floors by their stable slug (`min` over
|
|
78
|
-
* the topics, learned methods, figures or captioned equations the page shows, or the words of its lede).
|
|
79
|
-
*/
|
|
80
|
-
export const PAGES = {
|
|
81
|
-
'/introduction': {
|
|
82
|
-
name: 'Introduction',
|
|
83
|
-
// two top-level topics: the executive view first (the default), then the technical view
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
},
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
export const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
export const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
1
|
+
// The required areas of the documentation pages (ADR-0017 section 2.1), in ONE table. The gate's page library
|
|
2
|
+
// (inpage.mjs) measures what a section holds; this module says what each area must hold, and run.mjs applies it.
|
|
3
|
+
// docs/04_views.md transcribes the table and a unit test keeps the two in step: a rule is never written twice.
|
|
4
|
+
//
|
|
5
|
+
// A "topic" is a leaf panel of a page (a tab, a sub-tab or a tab-group item) that holds sections; a page without tabs is
|
|
6
|
+
// one topic. A tab list inside a section belongs to that section: its panels are not topics, and what they hold counts
|
|
7
|
+
// for the section across every state the gate visited.
|
|
8
|
+
// A "dense paragraph" is a visible `<p>` of at least DENSE_WORDS words in the displayed language, equations removed,
|
|
9
|
+
// counted once per distinct text. Hidden elements never count.
|
|
10
|
+
|
|
11
|
+
export const DENSE_WORDS = 40;
|
|
12
|
+
export const LEDE_WORDS = 35;
|
|
13
|
+
export const REASON_MIN = 12;
|
|
14
|
+
export const METHOD_KINDS = ['classical', 'learned'];
|
|
15
|
+
|
|
16
|
+
/** What a rule counts in a section: the key of the measurement taken by `G.docSections` and how it reads in a message. */
|
|
17
|
+
export const RULES = {
|
|
18
|
+
dense: { noun: 'dense paragraph(s)', min: true },
|
|
19
|
+
equations: { noun: 'captioned equation(s)', min: true },
|
|
20
|
+
figures: { noun: 'figure(s) with a caption', min: true },
|
|
21
|
+
callouts: { noun: 'callout(s)', min: true },
|
|
22
|
+
glossary: { noun: 'glossary item(s)', min: true },
|
|
23
|
+
steps: { noun: 'step(s) in an ordered list', min: true },
|
|
24
|
+
controls: { noun: 'control(s)', min: true },
|
|
25
|
+
refs: { noun: 'reference(s) cited', min: true },
|
|
26
|
+
stats: { noun: 'quantified statement(s) (value, unit, source)', min: true },
|
|
27
|
+
rows: { noun: 'row(s) of capability, outcome and proof point', min: true },
|
|
28
|
+
rowsNoStat: { noun: 'row(s) whose proof point is not a quantified statement', max: true },
|
|
29
|
+
items: { noun: 'item(s)', exact: true },
|
|
30
|
+
thinItems: { noun: 'item(s) without a sentence', max: true },
|
|
31
|
+
qa: { noun: 'question and answer pair(s)', min: true },
|
|
32
|
+
honest: { noun: 'honest callout(s)', exact: true },
|
|
33
|
+
honestOrStrong: { noun: 'honest or strong callout(s)', exact: true },
|
|
34
|
+
// derived: tables and charts are alternatives in some areas
|
|
35
|
+
table: { noun: 'table(s)', min: true },
|
|
36
|
+
chart: { noun: 'chart(s)', min: true },
|
|
37
|
+
tableOrChart: { noun: 'table(s) or chart(s)', min: true, of: ['tables', 'charts'] },
|
|
38
|
+
tableOrCode: { noun: 'table(s) or code block(s)', min: true, of: ['tables', 'codes'] },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const measureOf = (m, key) => {
|
|
42
|
+
const r = RULES[key];
|
|
43
|
+
if (r.of) return r.of.reduce((a, k) => a + (m[k] ?? 0), 0);
|
|
44
|
+
if (key === 'table') return m.tables ?? 0;
|
|
45
|
+
if (key === 'chart') return m.charts ?? 0;
|
|
46
|
+
if (key === 'honestOrStrong') return (m.honest ?? 0) + (m.strong ?? 0);
|
|
47
|
+
return m[key] ?? 0;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The floors a section fails. `m` is the measurement of the section (see `G.docSections`), `rule` the area's rule
|
|
52
|
+
* (`{ dense: 2, equations: 1, honest: 1 }`: a number is a minimum, or the exact count for `honest` and `honestOrStrong`).
|
|
53
|
+
* @returns {string[]} one text per failed rule, with the measured number and the floor
|
|
54
|
+
*/
|
|
55
|
+
export function failedRules(m, rule) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const [key, want] of Object.entries(rule)) {
|
|
58
|
+
const def = RULES[key];
|
|
59
|
+
if (!def) throw new Error(`gate/areas.mjs: unknown rule ${key}`);
|
|
60
|
+
const n = measureOf(m, key);
|
|
61
|
+
if (def.max ? n > want : def.exact ? n !== want : n < want) out.push(`${n} ${plural(def.noun, n)}, ${def.max ? `at most ${want} is allowed` : def.exact ? `exactly ${want} is required` : `the floor is ${want}`}`);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The rule as a sentence, for the docs and the gate's messages. */
|
|
67
|
+
export function describeRule(rule) {
|
|
68
|
+
return Object.entries(rule)
|
|
69
|
+
.map(([key, want]) => `${RULES[key].max ? 'at most' : RULES[key].exact ? 'exactly' : 'at least'} ${want} ${RULES[key].noun.replace('(s)', want === 1 ? '' : 's')}`)
|
|
70
|
+
.join(', ');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const A = (en, es, requires, extra = {}) => ({ what: { en, es }, requires, ...extra });
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The five tables of ADR-0017 section 2.1. `areas`: the vocabulary of the page (name, what it is in both languages,
|
|
77
|
+
* what the gate requires in a section of that area). `floors`: the page-level floors by their stable slug (`min` over
|
|
78
|
+
* the topics, learned methods, figures or captioned equations the page shows, or the words of its lede).
|
|
79
|
+
*/
|
|
80
|
+
export const PAGES = {
|
|
81
|
+
'/introduction': {
|
|
82
|
+
name: 'Introduction',
|
|
83
|
+
// two top-level topics: the executive view first (the default), then the technical view. The executive view has no
|
|
84
|
+
// areas: it is the shell's ExecutiveView, checked by executiveFailures below (0.12.0, problem-context-first.md).
|
|
85
|
+
views: ['executive', 'technical'],
|
|
86
|
+
areas: {
|
|
87
|
+
problem: A('the industrial or scientific problem, with the concrete equipment, setting and who it serves', 'el problema industrial o científico, con el equipo concreto, el contexto y a quién sirve', { dense: 2 }, { view: 'technical' }),
|
|
88
|
+
principles: A('the physics or the domain principles the product rests on', 'la física o los principios del dominio en que se apoya el producto', { dense: 2 }, { view: 'technical' }),
|
|
89
|
+
math: A('the governing mathematics', 'la matemática que gobierna', { equations: 1, glossary: 10 }, { view: 'technical' }),
|
|
90
|
+
pipeline: A('the end-to-end pipeline, from the data to the screen', 'el flujo completo, desde los datos hasta la pantalla', { steps: 4 }, { view: 'technical' }),
|
|
91
|
+
honesty: A('what is exact and what is illustrative; what the product is not', 'qué es exacto y qué es ilustrativo; qué no es el producto', { honest: 1, dense: 1 }, { view: 'technical' }),
|
|
92
|
+
},
|
|
93
|
+
floors: {
|
|
94
|
+
'intro-figure': { what: 'at least 1 overview figure in the technical view', measure: 'figures', min: 1, noun: 'figure(s) in the technical view' },
|
|
95
|
+
'intro-equations': { what: 'at least 3 captioned equations in the technical view', measure: 'equations', min: 3, noun: 'captioned equation(s) in the technical view' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
'/methodology': {
|
|
99
|
+
name: 'Methodology',
|
|
100
|
+
areas: {
|
|
101
|
+
method: A(
|
|
102
|
+
'one method family, as the build runs it: its exact behaviour and constants',
|
|
103
|
+
'una familia de métodos, tal como la ejecuta el producto: su comportamiento exacto y sus constantes',
|
|
104
|
+
{ dense: 4, equations: 2, figures: 1, honest: 1, refs: 1 },
|
|
105
|
+
{ perTopic: true, kinds: METHOD_KINDS },
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
floors: {
|
|
109
|
+
'method-topics': { what: 'at least 6 topics, one per method family', measure: 'topics', min: 6, noun: 'topic(s)' },
|
|
110
|
+
'method-learned': { what: 'at least 2 topics of kind learned', measure: 'learned', min: 2, noun: 'topic(s) of kind learned' },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
'/implementation': {
|
|
114
|
+
name: 'Implementation',
|
|
115
|
+
areas: {
|
|
116
|
+
architecture: A('offline pipeline, compact artifacts, the thin live lane, the browser', 'el flujo offline, los artefactos compactos, el carril vivo delgado y el navegador', { figures: 1 }),
|
|
117
|
+
reproducibility: A('determinism, seeds, versions, how a result is reproduced', 'determinismo, semillas, versiones, cómo se reproduce un resultado', { callouts: 1, dense: 1 }),
|
|
118
|
+
engine: A('the exact algorithm steps, constants, windows and scaling of one engine', 'los pasos exactos del algoritmo, las constantes, las ventanas y la escala de un motor', { dense: 2, equations: 1, callouts: 1 }),
|
|
119
|
+
lanes: A('the live and precompute boundary, and what admits a feature to the live lane', 'el límite entre lo vivo y lo precalculado, y qué admite una función en el carril vivo', { dense: 2 }),
|
|
120
|
+
contract: A('the artifact and data contracts', 'los contratos de artefactos y de datos', { tableOrCode: 1, dense: 1 }),
|
|
121
|
+
deployment: A('where and how it is deployed', 'dónde y cómo se despliega', { dense: 1 }),
|
|
122
|
+
},
|
|
123
|
+
floors: {
|
|
124
|
+
'impl-topics': { what: 'at least 8 topics', measure: 'topics', min: 8, noun: 'topic(s)' },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
'/experiments': {
|
|
128
|
+
name: 'Experiments',
|
|
129
|
+
areas: {
|
|
130
|
+
question: A('a distinct experimental question and its answer (one section per question)', 'una pregunta experimental distinta y su respuesta (una sección por pregunta)', { dense: 2 }),
|
|
131
|
+
metrics: A('the exact metric equations with their real constants', 'las ecuaciones exactas de las métricas con sus constantes reales', { equations: 2 }),
|
|
132
|
+
protocol: A('the leakage-safe, held-out protocol, with the forbidden anti-pattern shown', 'el protocolo sin fuga y con datos retenidos, con el antipatrón prohibido a la vista', { figures: 1 }),
|
|
133
|
+
datasets: A('the datasets, their redistribution class and their live or roadmap status', 'los conjuntos de datos, su clase de redistribución y su estado (vivo o en hoja de ruta)', { table: 1 }),
|
|
134
|
+
},
|
|
135
|
+
floors: {
|
|
136
|
+
'exp-topics': { what: 'at least 6 topics', measure: 'topics', min: 6, noun: 'topic(s)' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
'/benchmark': {
|
|
140
|
+
name: 'Benchmark',
|
|
141
|
+
areas: {
|
|
142
|
+
results: A('held-out numbers read from committed artifacts', 'cifras de datos retenidos, leídas de artefactos versionados', { tableOrChart: 1 }),
|
|
143
|
+
comparison: A('a learned model and a classical baseline on the same real data', 'un modelo aprendido y una línea base clásica sobre los mismos datos reales', { tableOrChart: 1 }),
|
|
144
|
+
robustness: A('an honest robustness or degradation curve', 'una curva honesta de robustez o degradación', { chart: 1 }),
|
|
145
|
+
live: A('a live in-browser inference panel on real held-out data', 'un panel de inferencia en vivo en el navegador sobre datos reales retenidos', { controls: 1 }),
|
|
146
|
+
provenance: A('per row: dataset, difficulty source, redistribution', 'por fila: conjunto de datos, fuente de la dificultad, redistribución', { table: 1 }),
|
|
147
|
+
caveat: A('the honest limits of the comparison', 'los límites honestos de la comparación', { honestOrStrong: 1 }),
|
|
148
|
+
},
|
|
149
|
+
floors: {},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Every documentation page: the lede. */
|
|
154
|
+
export const COMMON_FLOORS = {
|
|
155
|
+
'lede-words': { what: `a lede of at least ${LEDE_WORDS} words`, measure: 'ledeWords', min: LEDE_WORDS, noun: 'word(s) in the lede' },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const DOC_ROUTES = Object.keys(PAGES);
|
|
159
|
+
|
|
160
|
+
/** The floor ids a page's waiver may name: its areas, its page-level floors and the lede. */
|
|
161
|
+
export const waivable = (route) => [
|
|
162
|
+
...Object.entries(PAGES[route].areas).filter(([, a]) => a.view !== 'executive').map(([k]) => k),
|
|
163
|
+
...Object.keys(PAGES[route].floors),
|
|
164
|
+
...Object.keys(COMMON_FLOORS),
|
|
165
|
+
];
|
|
166
|
+
/** The executive view of the Introduction is the decision maker's page: none of its areas, and not the view, can be waived. */
|
|
167
|
+
export const NOT_WAIVABLE_NOTE = 'the executive view of the Introduction cannot be waived';
|
|
168
|
+
/** What a waiver may not name: the executive view and the four areas it replaced (0.12.0). */
|
|
169
|
+
export const EXECUTIVE_FLOORS = ['executive', 'executive-view', 'initiative', 'outcomes', 'positioning', 'credibility'];
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------------------------------------------ the executive view
|
|
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 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
|
+
export const EXEC_LIMITS = {
|
|
178
|
+
maxWords: 30,
|
|
179
|
+
minParagraphWords: 25,
|
|
180
|
+
equation: /\$|\\\(|\\\[|\\[a-zA-Z]+/,
|
|
181
|
+
citations: [/\(\s*Source/i, /\[\d+\]/, /\(\s*ECB/, /\(\w[^()]*\d{4}\)/],
|
|
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],
|
|
185
|
+
};
|
|
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. */
|
|
208
|
+
export const EXEC_SECTIONS = [
|
|
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' } },
|
|
214
|
+
];
|
|
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. */
|
|
219
|
+
export const EXEC_ILLUSTRATIONS = [
|
|
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.' } },
|
|
230
|
+
];
|
|
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
|
+
};
|
|
236
|
+
|
|
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. */
|
|
238
|
+
export const execSentenceLimit = (path) =>
|
|
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)
|
|
240
|
+
? 1
|
|
241
|
+
: path === 'key_statement'
|
|
242
|
+
? 2
|
|
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);
|
|
246
|
+
|
|
247
|
+
const sentencesOf = (text) =>
|
|
248
|
+
text
|
|
249
|
+
.split(/(?<=[.?!])\s+/)
|
|
250
|
+
.map((x) => x.trim())
|
|
251
|
+
.filter(Boolean);
|
|
252
|
+
const wordsIn = (text) => text.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
|
|
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
|
+
|
|
272
|
+
/**
|
|
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.
|
|
278
|
+
* @returns {string[]} one text per problem
|
|
279
|
+
*/
|
|
280
|
+
export function executiveFailures(list, lang) {
|
|
281
|
+
const L = lang.toUpperCase();
|
|
282
|
+
const out = [];
|
|
283
|
+
const bad = (m) => out.push(`Introduction, executive view (${L}): ${m}`);
|
|
284
|
+
const states = (list ?? []).filter(Boolean);
|
|
285
|
+
const first = states[0];
|
|
286
|
+
if (!first || first.count === 0) {
|
|
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)");
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
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
|
+
}
|
|
332
|
+
}
|
|
333
|
+
EXEC_SECTIONS.forEach((def, i) => {
|
|
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`);
|
|
343
|
+
});
|
|
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);
|
|
383
|
+
}
|
|
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);
|
|
389
|
+
const unreadable = [];
|
|
390
|
+
for (const f of fields.values()) {
|
|
391
|
+
if (!f.readable) {
|
|
392
|
+
unreadable.push(f.path);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const sentences = sentencesOf(f.text);
|
|
396
|
+
for (const sentence of sentences) {
|
|
397
|
+
const n = wordsIn(sentence);
|
|
398
|
+
if (n > EXEC_LIMITS.maxWords) bad(`field "${f.path}": a sentence of ${n} words; the limit is ${EXEC_LIMITS.maxWords}`);
|
|
399
|
+
}
|
|
400
|
+
const max = execSentenceLimit(f.path);
|
|
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}`);
|
|
404
|
+
if (EXEC_LIMITS.equation.test(f.text)) bad(`field "${f.path}": holds an equation or a TeX marker`);
|
|
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
|
+
}
|
|
410
|
+
}
|
|
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
|
+
|
|
441
|
+
// the three illustrations, in their places
|
|
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()) {
|
|
448
|
+
const want = EXEC_ILLUSTRATIONS.find((x) => x.name === ill.name);
|
|
449
|
+
const name = `illustration "${ill.name}"`;
|
|
450
|
+
if (want && ill.section !== want.section) bad(`${name} sits in section ${ill.section}, it belongs in section ${want.section}`);
|
|
451
|
+
if (ill.state === 'error') {
|
|
452
|
+
bad(`${name}: the file "${ill.file}" did not load (${ill.why})`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (!ill.svg) bad(`${name} is not an inline SVG`);
|
|
456
|
+
if (ill.img > 0) bad(`${name} is drawn through an img tag; it must be inline so it reads the theme`);
|
|
457
|
+
if (ill.chart > 0) bad(`${name} declares a chart (data-chart); the executive illustrations are drawings, not charts`);
|
|
458
|
+
if (ill.literal.length > 0) bad(`${name} uses literal colour(s) ${ill.literal.slice(0, 4).join(', ')}; colour only with var(--...) theme tokens, none or currentColor`);
|
|
459
|
+
if (ill.outside.length > 0) bad(`${name}: label(s) outside the viewBox: ${ill.outside.slice(0, 3).map((x) => `"${x}"`).join(', ')}`);
|
|
460
|
+
if (ill.labels === 0) bad(`${name} carries no label in this language`);
|
|
461
|
+
if (ill.small.length > 0) bad(`${name}: label(s) smaller than ${EXEC_LIMITS.minLabelPx}px as rendered: ${ill.small.slice(0, 3).join(', ')}`);
|
|
462
|
+
if (!ill.caption) bad(`${name} has no caption under it`);
|
|
463
|
+
}
|
|
464
|
+
return [...new Set(out)];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Parses `data-floor-waiver` ("floor: reason; floor: reason") into entries. */
|
|
468
|
+
export function parseWaivers(attr) {
|
|
469
|
+
return String(attr ?? '')
|
|
470
|
+
.split(';')
|
|
471
|
+
.map((x) => x.trim())
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.map((x) => {
|
|
474
|
+
const i = x.indexOf(':');
|
|
475
|
+
return i < 0 ? { floor: x, reason: '' } : { floor: x.slice(0, i).trim(), reason: x.slice(i + 1).trim() };
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const plural = (noun, n) => noun.replaceAll('(s)', n === 1 ? '' : 's');
|
|
480
|
+
|
|
481
|
+
/** What a waiver's reason holds that a reader can read: letters and digits, and the words that carry one (as the unavailable reason is judged). */
|
|
482
|
+
export const reasonStats = (reason) => ({
|
|
483
|
+
chars: reason.replace(/[^\p{L}\p{N}]+/gu, '').length,
|
|
484
|
+
words: reason.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length,
|
|
485
|
+
});
|
|
486
|
+
export const REASON_WORDS = 2;
|
|
487
|
+
const reasonOk = (reason) => {
|
|
488
|
+
const s = reasonStats(reason);
|
|
489
|
+
return s.chars >= REASON_MIN && s.words >= REASON_WORDS;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/** Sets of distinct things a section holds: seen again in another state they count once (a sub-tab shows others). */
|
|
493
|
+
const SET_KEYS = { dense: 'denseKeys', equations: 'equationKeys', figures: 'figureKeys', callouts: 'calloutKeys', honest: 'honestKeys', strong: 'strongKeys' };
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* One section seen in several states (the sub-tabs of a section show different parts of it): the union of what is
|
|
497
|
+
* distinct (dense paragraphs, equations, figures, callouts), the maximum of every other count. A fact without the
|
|
498
|
+
* distinct keys falls back to the maximum.
|
|
499
|
+
*/
|
|
500
|
+
export function mergeSections(list) {
|
|
501
|
+
const out = { ...list[0] };
|
|
502
|
+
for (const key of Object.keys(list[0])) if (typeof list[0][key] === 'number') out[key] = Math.max(...list.map((s) => s[key] ?? 0));
|
|
503
|
+
for (const [key, keys] of Object.entries(SET_KEYS)) {
|
|
504
|
+
if (list.every((s) => Array.isArray(s[keys]))) out[key] = new Set(list.flatMap((s) => s[keys])).size;
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Applies the table to what a documentation page showed across its visited states (`G.docPage` of gate/inpage.mjs, one
|
|
511
|
+
* entry per state) in one language. Pure: the runner passes the facts, this returns the failures and the waivers.
|
|
512
|
+
* @param {{ route: string, lang: string, states: object[], expectedTopics: number }} o
|
|
513
|
+
* @returns {{ failures: { trail: string, message: string }[], waivers: { floor: string, reason: string, valid: boolean }[], topics: number }}
|
|
514
|
+
*/
|
|
515
|
+
export function docFloorFailures({ route, lang, states, expectedTopics }) {
|
|
516
|
+
const P = PAGES[route];
|
|
517
|
+
const L = lang.toUpperCase();
|
|
518
|
+
const failures = [];
|
|
519
|
+
const bad = (trail, message) => failures.push({ trail, message });
|
|
520
|
+
const first = states[0];
|
|
521
|
+
if (!P || !first) return { failures, waivers: [], topics: 0 };
|
|
522
|
+
const NAME = P.name;
|
|
523
|
+
|
|
524
|
+
// ---- the waivers: parsed, validated, and applied only when valid
|
|
525
|
+
const entries = parseWaivers(first.waiver);
|
|
526
|
+
const ok = new Set(waivable(route));
|
|
527
|
+
const waivers = entries.map((w) => ({ ...w, valid: ok.has(w.floor) && reasonOk(w.reason) }));
|
|
528
|
+
for (const w of entries) {
|
|
529
|
+
if (!ok.has(w.floor)) {
|
|
530
|
+
const exec = route === '/introduction' && EXECUTIVE_FLOORS.includes(w.floor);
|
|
531
|
+
bad('', `${NAME} (${L}): the waiver for "${w.floor}" is refused: ${exec ? NOT_WAIVABLE_NOTE : `it names no floor of this page (${[...ok].join(', ')})`}`);
|
|
532
|
+
} else if (!reasonOk(w.reason)) {
|
|
533
|
+
const s = reasonStats(w.reason);
|
|
534
|
+
bad('', `${NAME} (${L}): the waiver for "${w.floor}" has a reason of ${w.reason.length} character(s) (${s.chars} letter(s) or digit(s) in ${s.words} word(s)), the floor is ${REASON_MIN} letters or digits in ${REASON_WORDS} words`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const waived = new Set(waivers.filter((w) => w.valid).map((w) => w.floor));
|
|
538
|
+
|
|
539
|
+
// ---- the topics and the sections, gathered across the states (a section seen twice counts once)
|
|
540
|
+
const topics = new Map();
|
|
541
|
+
const sections = new Map();
|
|
542
|
+
const figures = new Set();
|
|
543
|
+
const equations = new Set();
|
|
544
|
+
const execMessages = new Set();
|
|
545
|
+
const execStates = [];
|
|
546
|
+
for (const st of states) {
|
|
547
|
+
for (const t of st.topics) topics.set(t.key, t.label);
|
|
548
|
+
const ordinal = new Map();
|
|
549
|
+
for (const sec of st.sections) {
|
|
550
|
+
const n = ordinal.get(sec.topicKey) ?? 0;
|
|
551
|
+
ordinal.set(sec.topicKey, n + 1);
|
|
552
|
+
const k = `${sec.topicKey}#${n}`;
|
|
553
|
+
sections.set(k, [...(sections.get(k) ?? []), sec]);
|
|
554
|
+
}
|
|
555
|
+
if (st.view !== 'executive') {
|
|
556
|
+
for (const f of st.figures) figures.add(`${st.topics.map((t) => t.key).join('|')}::${f}`);
|
|
557
|
+
for (const e of st.equations) equations.add(`${st.topics.map((t) => t.key).join('|')}::${e}`);
|
|
558
|
+
}
|
|
559
|
+
if (route === '/introduction' && st.view !== 'executive' && st.executive && st.executive.count > 0) {
|
|
560
|
+
bad('', `${NAME} (${L}): the ExecutiveView sits in the "${st.view ?? 'none'}" view; it belongs to the executive view`);
|
|
561
|
+
}
|
|
562
|
+
if (route === '/introduction' && st.view === 'executive') {
|
|
563
|
+
execStates.push(st.executive);
|
|
564
|
+
if (st.viewEquations > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewEquations} equation(s); the executive view holds no equation`);
|
|
565
|
+
if (st.viewGlossaries > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewGlossaries} symbol glossary(ies); the executive view holds none`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (route === '/introduction' && execStates.length > 0) for (const m of executiveFailures(execStates, lang)) execMessages.add(m);
|
|
569
|
+
for (const m of execMessages) bad('', m);
|
|
570
|
+
const seen = [...sections.values()].map(mergeSections);
|
|
571
|
+
const tname = (label) => (label ? `topic "${label}"` : 'the page');
|
|
572
|
+
|
|
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`);
|
|
574
|
+
|
|
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`);
|
|
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}`);
|
|
578
|
+
|
|
579
|
+
// ---- the Introduction's views: the executive one first
|
|
580
|
+
if (route === '/introduction') {
|
|
581
|
+
const v = first.views ?? [];
|
|
582
|
+
if (!v.includes('executive')) bad('', `${NAME} (${L}): no executive view; the top-level topics are [${v.join(', ') || 'none'}] and the executive view must be the first (ADR-0017 2.1)`);
|
|
583
|
+
else if (v[0] !== 'executive') bad('', `${NAME} (${L}): the executive view is not the first topic (the first is "${v[0]}")`);
|
|
584
|
+
if (v.includes('executive') && first.view !== 'executive') bad('', `${NAME} (${L}): the executive view is not the view the page opens on (it opens on "${first.view ?? 'none'}")`);
|
|
585
|
+
if (!v.includes('technical')) bad('', `${NAME} (${L}): no technical view; the top-level topics are [${v.join(', ') || 'none'}]`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ---- each section that declares an area
|
|
589
|
+
const present = new Set();
|
|
590
|
+
for (const sec of seen) {
|
|
591
|
+
if (route === '/introduction' && (first.views ?? []).length > 0 && sec.view !== 'executive' && sec.view !== 'technical') {
|
|
592
|
+
bad(sec.topic, `${NAME} (${L}): section "${sec.title}" sits outside the two views (the executive view and the technical view hold every section of the Introduction)`);
|
|
593
|
+
}
|
|
594
|
+
if (!sec.area) continue;
|
|
595
|
+
const def = P.areas[sec.area];
|
|
596
|
+
if (!def) {
|
|
597
|
+
bad(sec.topic, `${NAME}, ${tname(sec.topic)} (${L}): section "${sec.title}" declares the unknown area "${sec.area}" (this page knows: ${Object.keys(P.areas).join(', ')})`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
present.add(sec.area);
|
|
601
|
+
if (waived.has(sec.area)) continue;
|
|
602
|
+
if (def.view && (def.view === 'executive') !== (sec.view === 'executive')) {
|
|
603
|
+
bad(sec.topic, `${NAME}, ${sec.area} area, ${tname(sec.topic)} (${L}): the area belongs to the ${def.view} view and sits in the ${sec.view === 'executive' ? 'executive' : 'technical'} view`);
|
|
604
|
+
}
|
|
605
|
+
if (def.kinds && !def.kinds.includes(sec.kind)) bad(sec.topic, `${NAME}, ${sec.area} area, ${tname(sec.topic)} (${L}): section "${sec.title}" has the kind "${sec.kind ?? 'none'}", expected ${def.kinds.join(' or ')}`);
|
|
606
|
+
for (const t of failedRules(sec, def.requires)) bad(sec.topic, `${NAME}, ${sec.area} area, ${tname(sec.topic)} (${L}): ${t}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ---- every area of the page is declared at least once
|
|
610
|
+
for (const [name, def] of Object.entries(P.areas)) {
|
|
611
|
+
if (waived.has(name) || present.has(name)) continue;
|
|
612
|
+
bad('', `${NAME} (${L}): no section declares the area "${name}" (${def.what.en})`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ---- Methodology: one method section per topic
|
|
616
|
+
if (route === '/methodology' && !waived.has('method')) {
|
|
617
|
+
for (const [key, label] of topics) {
|
|
618
|
+
const n = seen.filter((s) => s.topicKey === key && s.area === 'method').length;
|
|
619
|
+
if (n !== 1) bad(label, `${NAME}, ${tname(label)} (${L}): ${n} method section(s), exactly 1 is required`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ---- the page-level floors
|
|
624
|
+
const kinds = new Set(seen.filter((s) => s.area === 'method' && s.kind === 'learned').map((s) => s.topicKey));
|
|
625
|
+
const measured = { topics: topics.size, learned: kinds.size, figures: figures.size, equations: equations.size };
|
|
626
|
+
for (const [id, f] of Object.entries(P.floors)) {
|
|
627
|
+
if (waived.has(id)) continue;
|
|
628
|
+
if (measured[f.measure] < f.min) bad('', `${NAME} (${L}): ${measured[f.measure]} ${plural(f.noun, measured[f.measure])}, the floor is ${f.min}`);
|
|
629
|
+
}
|
|
630
|
+
return { failures, waivers, topics: topics.size };
|
|
631
|
+
}
|