@fasl-work/caos-app-shell 0.10.0 → 0.11.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useThemeStore, useShellLang, useLangStore, SHELL_TOKENS, pick, useStageSize, NOT_AVAILABLE, formatTicks, formatNumber, NBSP } from './chunk-DX36XRD5.js';
2
2
  export { NBSP, NOT_AVAILABLE, SCIENTIFIC_BELOW, SHELL_TOKENS, Stage, applyTheme, formatNumber, formatTicks, pick, readTheme, resolveToken, useFormat, useLangStore, useShellLang, useStageSize, useThemeStore, useThemeTokens } from './chunk-DX36XRD5.js';
3
3
  export { LANG_STORAGE_KEY, STANDARD_ROUTES, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from './chunk-X6Y5CDUD.js';
4
- import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo, Children } from 'react';
4
+ import { createContext, Component, useState, useRef, useId, useEffect, useCallback, useContext, useMemo, useLayoutEffect, Children } from 'react';
5
5
  import { useInRouterContext, NavLink, useLocation } from 'react-router';
6
6
  import { Sun, Moon, Languages, Boxes, CodeXml, Globe, Briefcase, Info } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -799,6 +799,135 @@ function CaseSelector(props) {
799
799
  function caseKindOf(c) {
800
800
  return c.kind ?? "synthetic";
801
801
  }
802
+ var CitationsContext = createContext({ list: [], byId: {} });
803
+ function CitationsProvider({ items, children }) {
804
+ const byId = {};
805
+ for (const c of items) {
806
+ if (byId[c.id]) console.error(`[caos-app-shell] duplicate citation id ${c.id}`);
807
+ if (!c.doi && !c.url) console.error(`[caos-app-shell] citation ${c.id} has no doi or url (ADR-0017 s4)`);
808
+ byId[c.id] = c;
809
+ }
810
+ return /* @__PURE__ */ jsx(CitationsContext.Provider, { value: { list: items, byId }, children });
811
+ }
812
+ function href(c) {
813
+ return c.doi ? `https://doi.org/${c.doi}` : c.url;
814
+ }
815
+ function Cite({ id, paren = true }) {
816
+ const { byId } = useContext(CitationsContext);
817
+ const lang = useShellLang();
818
+ const c = byId[id];
819
+ if (!c) {
820
+ console.error(`[caos-app-shell] Cite of an unknown citation id ${id}`);
821
+ return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
822
+ "[",
823
+ id,
824
+ "]"
825
+ ] });
826
+ }
827
+ const h = href(c);
828
+ const text = pick(c.label, lang);
829
+ const label = h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", children: text }) : /* @__PURE__ */ jsx("span", { children: text });
830
+ return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
831
+ paren ? "(" : null,
832
+ label,
833
+ paren ? ")" : null
834
+ ] });
835
+ }
836
+ function firstAppearance(sections) {
837
+ const seen = /* @__PURE__ */ new Set();
838
+ const out = [];
839
+ for (const ids of sections) {
840
+ for (const id of ids) {
841
+ if (!seen.has(id)) {
842
+ seen.add(id);
843
+ out.push(id);
844
+ }
845
+ }
846
+ }
847
+ return out;
848
+ }
849
+ var RefsScopeContext = createContext(null);
850
+ function useRefsScope() {
851
+ return useContext(RefsScopeContext);
852
+ }
853
+ function DocRefsScope({ children }) {
854
+ const entries = useRef(/* @__PURE__ */ new Map());
855
+ const [ids, setIds] = useState([]);
856
+ const recompute = useCallback(() => {
857
+ const rows = [...entries.current.values()].filter((r) => r.ids.length > 0);
858
+ rows.sort((a, b) => {
859
+ if (!a.el || !b.el || a.el === b.el) return 0;
860
+ return a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
861
+ });
862
+ const next = firstAppearance(rows.map((r) => r.ids));
863
+ setIds((prev) => prev.length === next.length && prev.every((id, i) => id === next[i]) ? prev : next);
864
+ }, []);
865
+ const api = useMemo(
866
+ () => ({
867
+ register(key, el, list) {
868
+ entries.current.set(key, { el, ids: list });
869
+ recompute();
870
+ },
871
+ unregister(key) {
872
+ entries.current.delete(key);
873
+ recompute();
874
+ }
875
+ }),
876
+ [recompute]
877
+ );
878
+ return /* @__PURE__ */ jsxs(RefsScopeContext.Provider, { value: api, children: [
879
+ children,
880
+ /* @__PURE__ */ jsx(DocReferences, { ids })
881
+ ] });
882
+ }
883
+ function DocReferences({ ids }) {
884
+ const { byId } = useContext(CitationsContext);
885
+ const lang = useShellLang();
886
+ const items = ids.filter((id) => {
887
+ if (byId[id]) return true;
888
+ console.error(`[caos-app-shell] a section lists the unknown citation id ${id}`);
889
+ return false;
890
+ });
891
+ if (items.length === 0) return null;
892
+ return /* @__PURE__ */ jsxs("section", { className: "caos-doc-references", "data-doc-references": "", children: [
893
+ /* @__PURE__ */ jsx("h2", { children: lang === "es" ? "Referencias" : "References" }),
894
+ /* @__PURE__ */ jsx("ol", { className: "caos-doc-reference-list", children: items.map((id) => {
895
+ const c = byId[id];
896
+ const h = href(c);
897
+ return /* @__PURE__ */ jsxs("li", { children: [
898
+ /* @__PURE__ */ jsx("span", { children: c.citation }),
899
+ " ",
900
+ h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? `doi:${c.doi}` : "link" }) : null
901
+ ] }, id);
902
+ }) })
903
+ ] });
904
+ }
905
+ function Refs({ ids, label }) {
906
+ return /* @__PURE__ */ jsxs("p", { className: "th-refs", children: [
907
+ /* @__PURE__ */ jsx("span", { className: "th-refs-label", children: label }),
908
+ " ",
909
+ ids.map((id, i) => /* @__PURE__ */ jsxs("span", { children: [
910
+ i > 0 ? " \xB7 " : null,
911
+ /* @__PURE__ */ jsx(Cite, { id, paren: false })
912
+ ] }, id))
913
+ ] });
914
+ }
915
+ function ReferenceList({ ids, heading }) {
916
+ console.error("[caos-app-shell] ReferenceList is superseded by the end-of-page references block (ADR-0017 s4): give each DocSection its refs");
917
+ const { list, byId } = useContext(CitationsContext);
918
+ const items = ids ? ids.map((k) => byId[k]).filter(Boolean) : list;
919
+ return /* @__PURE__ */ jsxs("section", { className: "references", "aria-label": heading ?? "References", children: [
920
+ heading ? /* @__PURE__ */ jsx("h2", { children: heading }) : null,
921
+ /* @__PURE__ */ jsx("ol", { className: "reference-list", children: items.map((c) => {
922
+ const h = href(c);
923
+ return /* @__PURE__ */ jsxs("li", { children: [
924
+ /* @__PURE__ */ jsx("span", { children: c.citation }),
925
+ " ",
926
+ h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? `doi:${c.doi}` : "link" }) : null
927
+ ] }, c.id);
928
+ }) })
929
+ ] });
930
+ }
802
931
  var MAX_PEER_TABS = 6;
803
932
  function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
804
933
  const baseId = useId();
@@ -859,7 +988,7 @@ function Tabs({ tabs, initial, value, onChange, ariaLabel }) {
859
988
  tabIndex: 0,
860
989
  className: "tabpanel",
861
990
  "data-panel": current.id,
862
- children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: current.content })
991
+ children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) })
863
992
  },
864
993
  current.id
865
994
  )
@@ -937,7 +1066,7 @@ function SubTabs({ tabs, initial, value, onChange, ariaLabel, orientation = "hor
937
1066
  tabIndex: 0,
938
1067
  className: "subtabpanel",
939
1068
  "data-panel": current.id,
940
- children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: current.content })
1069
+ children: /* @__PURE__ */ jsx(PanelBoundary, { panel: current.id, children: /* @__PURE__ */ jsx(DocRefsScope, { children: current.content }) })
941
1070
  },
942
1071
  current.id
943
1072
  ) })
@@ -982,7 +1111,7 @@ function Callout({
982
1111
  children
983
1112
  }) {
984
1113
  const cls = variant === "honest" ? "callout callout-honest" : variant === "strong" ? "callout callout-strong" : "callout callout-note";
985
- return /* @__PURE__ */ jsxs("div", { className: cls, children: [
1114
+ return /* @__PURE__ */ jsxs("div", { className: cls, "data-callout": variant, children: [
986
1115
  title && /* @__PURE__ */ jsx("p", { className: "callout-title", children: title }),
987
1116
  /* @__PURE__ */ jsx("div", { className: "callout-body", children })
988
1117
  ] });
@@ -1007,94 +1136,82 @@ function Figure({ caption, children }) {
1007
1136
  caption && /* @__PURE__ */ jsx("figcaption", { className: "figure-caption", children: caption })
1008
1137
  ] });
1009
1138
  }
1010
- var CitationsContext = createContext({ list: [], byId: {} });
1011
- function CitationsProvider({ items, children }) {
1012
- const byId = {};
1013
- for (const c of items) {
1014
- if (byId[c.id]) console.error(`[caos-app-shell] duplicate citation id ${c.id}`);
1015
- if (!c.doi && !c.url) console.error(`[caos-app-shell] citation ${c.id} has no doi or url (ADR-0017 s4)`);
1016
- byId[c.id] = c;
1017
- }
1018
- return /* @__PURE__ */ jsx(CitationsContext.Provider, { value: { list: items, byId }, children });
1019
- }
1020
- function href(c) {
1021
- return c.doi ? `https://doi.org/${c.doi}` : c.url;
1022
- }
1023
- function Cite({ id, paren = true }) {
1024
- const { byId } = useContext(CitationsContext);
1025
- const lang = useShellLang();
1026
- const c = byId[id];
1027
- if (!c) {
1028
- console.error(`[caos-app-shell] Cite of an unknown citation id ${id}`);
1029
- return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
1030
- "[",
1031
- id,
1032
- "]"
1033
- ] });
1034
- }
1035
- const h = href(c);
1036
- const text = pick(c.label, lang);
1037
- const label = h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", children: text }) : /* @__PURE__ */ jsx("span", { children: text });
1038
- return /* @__PURE__ */ jsxs("cite", { className: "cite-inline", children: [
1039
- paren ? "(" : null,
1040
- label,
1041
- paren ? ")" : null
1042
- ] });
1043
- }
1044
- function Refs({ ids, label }) {
1045
- return /* @__PURE__ */ jsxs("p", { className: "th-refs", children: [
1046
- /* @__PURE__ */ jsx("span", { className: "th-refs-label", children: label }),
1047
- " ",
1048
- ids.map((id, i) => /* @__PURE__ */ jsxs("span", { children: [
1049
- i > 0 ? " \xB7 " : null,
1050
- /* @__PURE__ */ jsx(Cite, { id, paren: false })
1051
- ] }, id))
1052
- ] });
1053
- }
1054
- function ReferenceList({ ids, heading }) {
1055
- console.error("[caos-app-shell] ReferenceList is banned (ADR-0017 s4): end each section with Refs");
1056
- const { list, byId } = useContext(CitationsContext);
1057
- const items = ids ? ids.map((k) => byId[k]).filter(Boolean) : list;
1058
- return /* @__PURE__ */ jsxs("section", { className: "references", "aria-label": heading ?? "References", children: [
1059
- heading ? /* @__PURE__ */ jsx("h2", { children: heading }) : null,
1060
- /* @__PURE__ */ jsx("ol", { className: "reference-list", children: items.map((c) => {
1061
- const h = href(c);
1062
- return /* @__PURE__ */ jsxs("li", { children: [
1063
- /* @__PURE__ */ jsx("span", { children: c.citation }),
1064
- " ",
1065
- h ? /* @__PURE__ */ jsx("a", { href: h, target: "_blank", rel: "noreferrer noopener", className: "faint", children: c.doi ? `doi:${c.doi}` : "link" }) : null
1066
- ] }, c.id);
1067
- }) })
1068
- ] });
1069
- }
1070
- function DocPage({ title, lede, wide, children }) {
1139
+ var DOC_AREAS = {
1140
+ "/introduction": ["initiative", "outcomes", "positioning", "credibility", "problem", "principles", "math", "pipeline", "honesty"],
1141
+ "/methodology": ["method"],
1142
+ "/implementation": ["architecture", "reproducibility", "engine", "lanes", "contract", "deployment"],
1143
+ "/experiments": ["question", "metrics", "protocol", "datasets"],
1144
+ "/benchmark": ["results", "comparison", "robustness", "live", "provenance", "caveat"]
1145
+ };
1146
+ function DocPage({ title, lede, wide, floorWaivers, children }) {
1071
1147
  const lang = useShellLang();
1072
- return /* @__PURE__ */ jsxs("div", { className: wide ? "page-body wide prose caos-doc" : "page-body prose caos-doc", "data-doc-page": "", children: [
1148
+ const waivers = (floorWaivers ?? []).map((w) => `${w.floor}: ${pick(w.reason, lang)}`).join("; ");
1149
+ return /* @__PURE__ */ jsxs("div", { className: wide ? "page-body wide prose caos-doc" : "page-body prose caos-doc", "data-doc-page": "", "data-floor-waiver": waivers || void 0, children: [
1073
1150
  /* @__PURE__ */ jsxs("div", { className: "page-head", children: [
1074
1151
  /* @__PURE__ */ jsx("h1", { children: pick(title, lang) }),
1075
1152
  /* @__PURE__ */ jsx("p", { className: "lede", children: lede })
1076
1153
  ] }),
1077
- children
1154
+ /* @__PURE__ */ jsx(DocRefsScope, { children })
1078
1155
  ] });
1079
1156
  }
1080
- function DocSection({ title, id, refs, noRefsReason, children }) {
1157
+ function DocSection({ title, id, area, kind, refs, noRefsReason, children }) {
1081
1158
  const lang = useShellLang();
1082
1159
  const hasRefs = Boolean(refs && refs.length > 0);
1160
+ const scope = useRefsScope();
1161
+ const key = useId();
1162
+ const el = useRef(null);
1163
+ const idsKey = (refs ?? []).join("\n");
1164
+ useLayoutEffect(() => {
1165
+ if (!scope || !idsKey) return void 0;
1166
+ scope.register(key, el.current, idsKey.split("\n"));
1167
+ return () => scope.unregister(key);
1168
+ }, [scope, key, idsKey]);
1083
1169
  return /* @__PURE__ */ jsxs(
1084
1170
  "section",
1085
1171
  {
1172
+ ref: el,
1086
1173
  id,
1087
1174
  className: "caos-doc-section",
1088
1175
  "data-doc-section": "",
1176
+ "data-area": area,
1177
+ "data-method-kind": kind,
1089
1178
  "data-no-refs-reason": hasRefs ? void 0 : pick(noRefsReason, lang) || "missing",
1179
+ "data-refs": hasRefs ? refs.length : void 0,
1090
1180
  children: [
1091
1181
  /* @__PURE__ */ jsx("h2", { children: pick(title, lang) }),
1092
- children,
1093
- hasRefs && /* @__PURE__ */ jsx(Refs, { ids: refs, label: lang === "es" ? "Referencias" : "References" })
1182
+ children
1094
1183
  ]
1095
1184
  }
1096
1185
  );
1097
1186
  }
1187
+ function Glossary({ entries, ariaLabel }) {
1188
+ const lang = useShellLang();
1189
+ return /* @__PURE__ */ jsx("dl", { className: "caos-glossary", "data-glossary": "", "aria-label": ariaLabel === void 0 ? void 0 : pick(ariaLabel, lang), children: entries.map((e, i) => /* @__PURE__ */ jsxs("div", { className: "caos-glossary-item", "data-glossary-item": "", children: [
1190
+ /* @__PURE__ */ jsx("dt", { children: pick(e.term, lang) }),
1191
+ /* @__PURE__ */ jsx("dd", { children: pick(e.definition, lang) })
1192
+ ] }, `${pick(e.term, "en")}-${i}`)) });
1193
+ }
1194
+ function Stat({ value, unit, source }) {
1195
+ const lang = useShellLang();
1196
+ const src = pick(source, lang);
1197
+ return /* @__PURE__ */ jsxs("span", { className: "caos-stat", "data-stat": "", "data-stat-unit": pick(unit, lang), "data-stat-source": src, children: [
1198
+ /* @__PURE__ */ jsx("strong", { className: "caos-stat-value", children: pick(value, lang) }),
1199
+ " ",
1200
+ /* @__PURE__ */ jsx("span", { className: "caos-stat-unit", children: pick(unit, lang) }),
1201
+ " ",
1202
+ /* @__PURE__ */ jsxs("span", { className: "caos-stat-source", children: [
1203
+ "(",
1204
+ src,
1205
+ ")"
1206
+ ] })
1207
+ ] });
1208
+ }
1209
+ function Question({ question, answer }) {
1210
+ return /* @__PURE__ */ jsxs("div", { className: "caos-qa", "data-qa": "", children: [
1211
+ /* @__PURE__ */ jsx("p", { className: "caos-qa-question", "data-question": "", children: question }),
1212
+ /* @__PURE__ */ jsx("div", { className: "caos-qa-answer", "data-answer": "", children: answer })
1213
+ ] });
1214
+ }
1098
1215
  function replaceQueryParam(param, value) {
1099
1216
  if (typeof window === "undefined") return;
1100
1217
  const next = withCaseParam(window.location.search, value, param);
@@ -1748,4 +1865,4 @@ function ViewsRow({ children, shares }) {
1748
1865
  }) });
1749
1866
  }
1750
1867
 
1751
- export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DocPage, DocSection, Equation, Figure, FocusShell, Gauge, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Readout, ReferenceList, Refs, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, viewParamOf, widestLabel, withCaseParam };
1868
+ export { AppShell, ArchitectureModal, BREAKPOINTS, BarChart, Callout, CaseSelector, CaseWorkbench, ChipGroup, CitationsProvider, Cite, DOC_AREAS, DocPage, DocReferences, DocRefsScope, DocSection, Equation, Figure, FocusShell, Gauge, Glossary, InlineMath, Knob, LaneBadge, LanguageToggle, MAX_PEER_TABS, MAX_WORKBENCH_GROUPS, PanelBoundary, PlotCard, Question, Readout, ReferenceList, Refs, Stat, SubTabs, SurfacePage, TabGroups, Tabs, ThemeToggle, VariantBar, Verdict, ViewsRow, WorkbenchLayout, WorkbenchShell, WorkbenchStateContext, caseKind, caseTooltip, casesInSource, createVizLoop, firstAppearance, fitLabel, fontFamily, groupByCategory, isStale, makeStateKey, niceStep, niceTicks, readCaseParam, replaceQueryParam, sourcesPresent, textWidth, usePausedViz, useUrlView, useWorkbenchState, validateArchitectureConfig, viewParamOf, widestLabel, withCaseParam };
package/gate/areas.mjs ADDED
@@ -0,0 +1,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
84
+ views: ['executive', 'technical'],
85
+ areas: {
86
+ initiative: A('the initiative an executive owns, named by role and industry, and the quantified reason to act now', 'la iniciativa que un ejecutivo tiene a su cargo, nombrada por rol e industria, y la razón cuantificada para actuar ahora', { dense: 1, stats: 1 }, { view: 'executive' }),
87
+ outcomes: A('the transformation story: the vision first, then each capability tied to a business outcome with a quantified proof point', 'la historia de transformación: primero la visión, luego cada capacidad atada a un resultado de negocio con una prueba cuantificada', { dense: 1, rows: 3, rowsNoStat: 0 }, { view: 'executive' }),
88
+ positioning: A('the value story: four value points, each tied back to the problem of that executive', 'la historia de valor: cuatro puntos de valor, cada uno atado al problema de ese ejecutivo', { items: 4, thinItems: 0 }, { view: 'executive' }),
89
+ credibility: A('delivery and credibility: the business questions that executive would ask, each answered plainly with the honest limits', 'entrega y credibilidad: las preguntas de negocio que haría ese ejecutivo, cada una respondida con claridad y con los límites honestos', { qa: 3, honest: 1 }, { view: 'executive' }),
90
+ 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' }),
91
+ 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' }),
92
+ math: A('the governing mathematics', 'la matemática que gobierna', { equations: 1, glossary: 10 }, { view: 'technical' }),
93
+ 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' }),
94
+ 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' }),
95
+ },
96
+ floors: {
97
+ 'intro-figure': { what: 'at least 1 overview figure in the technical view', measure: 'figures', min: 1, noun: 'figure(s) in the technical view' },
98
+ 'intro-equations': { what: 'at least 3 captioned equations in the technical view', measure: 'equations', min: 3, noun: 'captioned equation(s) in the technical view' },
99
+ },
100
+ },
101
+ '/methodology': {
102
+ name: 'Methodology',
103
+ areas: {
104
+ method: A(
105
+ 'one method family, as the build runs it: its exact behaviour and constants',
106
+ 'una familia de métodos, tal como la ejecuta el producto: su comportamiento exacto y sus constantes',
107
+ { dense: 4, equations: 2, figures: 1, honest: 1, refs: 1 },
108
+ { perTopic: true, kinds: METHOD_KINDS },
109
+ ),
110
+ },
111
+ floors: {
112
+ 'method-topics': { what: 'at least 6 topics, one per method family', measure: 'topics', min: 6, noun: 'topic(s)' },
113
+ 'method-learned': { what: 'at least 2 topics of kind learned', measure: 'learned', min: 2, noun: 'topic(s) of kind learned' },
114
+ },
115
+ },
116
+ '/implementation': {
117
+ name: 'Implementation',
118
+ areas: {
119
+ 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 }),
120
+ reproducibility: A('determinism, seeds, versions, how a result is reproduced', 'determinismo, semillas, versiones, cómo se reproduce un resultado', { callouts: 1, dense: 1 }),
121
+ 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 }),
122
+ 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 }),
123
+ contract: A('the artifact and data contracts', 'los contratos de artefactos y de datos', { tableOrCode: 1, dense: 1 }),
124
+ deployment: A('where and how it is deployed', 'dónde y cómo se despliega', { dense: 1 }),
125
+ },
126
+ floors: {
127
+ 'impl-topics': { what: 'at least 8 topics', measure: 'topics', min: 8, noun: 'topic(s)' },
128
+ },
129
+ },
130
+ '/experiments': {
131
+ name: 'Experiments',
132
+ areas: {
133
+ 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 }),
134
+ metrics: A('the exact metric equations with their real constants', 'las ecuaciones exactas de las métricas con sus constantes reales', { equations: 2 }),
135
+ 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 }),
136
+ 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 }),
137
+ },
138
+ floors: {
139
+ 'exp-topics': { what: 'at least 6 topics', measure: 'topics', min: 6, noun: 'topic(s)' },
140
+ },
141
+ },
142
+ '/benchmark': {
143
+ name: 'Benchmark',
144
+ areas: {
145
+ results: A('held-out numbers read from committed artifacts', 'cifras de datos retenidos, leídas de artefactos versionados', { tableOrChart: 1 }),
146
+ 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 }),
147
+ robustness: A('an honest robustness or degradation curve', 'una curva honesta de robustez o degradación', { chart: 1 }),
148
+ 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 }),
149
+ provenance: A('per row: dataset, difficulty source, redistribution', 'por fila: conjunto de datos, fuente de la dificultad, redistribución', { table: 1 }),
150
+ caveat: A('the honest limits of the comparison', 'los límites honestos de la comparación', { honestOrStrong: 1 }),
151
+ },
152
+ floors: {},
153
+ },
154
+ };
155
+
156
+ /** Every documentation page: the lede. */
157
+ export const COMMON_FLOORS = {
158
+ 'lede-words': { what: `a lede of at least ${LEDE_WORDS} words`, measure: 'ledeWords', min: LEDE_WORDS, noun: 'word(s) in the lede' },
159
+ };
160
+
161
+ export const DOC_ROUTES = Object.keys(PAGES);
162
+
163
+ /** The floor ids a page's waiver may name: its areas, its page-level floors and the lede. */
164
+ export const waivable = (route) => [
165
+ ...Object.entries(PAGES[route].areas).filter(([, a]) => a.view !== 'executive').map(([k]) => k),
166
+ ...Object.keys(PAGES[route].floors),
167
+ ...Object.keys(COMMON_FLOORS),
168
+ ];
169
+ /** The executive view of the Introduction is the decision maker's page: none of its areas, and not the view, can be waived. */
170
+ export const NOT_WAIVABLE_NOTE = 'the executive view of the Introduction cannot be waived';
171
+
172
+ /** Parses `data-floor-waiver` ("floor: reason; floor: reason") into entries. */
173
+ export function parseWaivers(attr) {
174
+ return String(attr ?? '')
175
+ .split(';')
176
+ .map((x) => x.trim())
177
+ .filter(Boolean)
178
+ .map((x) => {
179
+ const i = x.indexOf(':');
180
+ return i < 0 ? { floor: x, reason: '' } : { floor: x.slice(0, i).trim(), reason: x.slice(i + 1).trim() };
181
+ });
182
+ }
183
+
184
+ const plural = (noun, n) => noun.replaceAll('(s)', n === 1 ? '' : 's');
185
+
186
+ /** 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). */
187
+ export const reasonStats = (reason) => ({
188
+ chars: reason.replace(/[^\p{L}\p{N}]+/gu, '').length,
189
+ words: reason.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length,
190
+ });
191
+ export const REASON_WORDS = 2;
192
+ const reasonOk = (reason) => {
193
+ const s = reasonStats(reason);
194
+ return s.chars >= REASON_MIN && s.words >= REASON_WORDS;
195
+ };
196
+
197
+ /** Sets of distinct things a section holds: seen again in another state they count once (a sub-tab shows others). */
198
+ const SET_KEYS = { dense: 'denseKeys', equations: 'equationKeys', figures: 'figureKeys', callouts: 'calloutKeys', honest: 'honestKeys', strong: 'strongKeys' };
199
+
200
+ /**
201
+ * One section seen in several states (the sub-tabs of a section show different parts of it): the union of what is
202
+ * distinct (dense paragraphs, equations, figures, callouts), the maximum of every other count. A fact without the
203
+ * distinct keys falls back to the maximum.
204
+ */
205
+ export function mergeSections(list) {
206
+ const out = { ...list[0] };
207
+ for (const key of Object.keys(list[0])) if (typeof list[0][key] === 'number') out[key] = Math.max(...list.map((s) => s[key] ?? 0));
208
+ for (const [key, keys] of Object.entries(SET_KEYS)) {
209
+ if (list.every((s) => Array.isArray(s[keys]))) out[key] = new Set(list.flatMap((s) => s[keys])).size;
210
+ }
211
+ return out;
212
+ }
213
+
214
+ /**
215
+ * Applies the table to what a documentation page showed across its visited states (`G.docPage` of gate/inpage.mjs, one
216
+ * entry per state) in one language. Pure: the runner passes the facts, this returns the failures and the waivers.
217
+ * @param {{ route: string, lang: string, states: object[], expectedTopics: number }} o
218
+ * @returns {{ failures: { trail: string, message: string }[], waivers: { floor: string, reason: string, valid: boolean }[], topics: number }}
219
+ */
220
+ export function docFloorFailures({ route, lang, states, expectedTopics }) {
221
+ const P = PAGES[route];
222
+ const L = lang.toUpperCase();
223
+ const failures = [];
224
+ const bad = (trail, message) => failures.push({ trail, message });
225
+ const first = states[0];
226
+ if (!P || !first) return { failures, waivers: [], topics: 0 };
227
+ const NAME = P.name;
228
+
229
+ // ---- the waivers: parsed, validated, and applied only when valid
230
+ const entries = parseWaivers(first.waiver);
231
+ const ok = new Set(waivable(route));
232
+ const waivers = entries.map((w) => ({ ...w, valid: ok.has(w.floor) && reasonOk(w.reason) }));
233
+ for (const w of entries) {
234
+ if (!ok.has(w.floor)) {
235
+ const exec = P.areas[w.floor]?.view === 'executive';
236
+ bad('', `${NAME} (${L}): the waiver for "${w.floor}" is refused: ${exec ? NOT_WAIVABLE_NOTE : `it names no floor of this page (${[...ok].join(', ')})`}`);
237
+ } else if (!reasonOk(w.reason)) {
238
+ const s = reasonStats(w.reason);
239
+ 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`);
240
+ }
241
+ }
242
+ const waived = new Set(waivers.filter((w) => w.valid).map((w) => w.floor));
243
+
244
+ // ---- the topics and the sections, gathered across the states (a section seen twice counts once)
245
+ const topics = new Map();
246
+ const sections = new Map();
247
+ const figures = new Set();
248
+ const equations = new Set();
249
+ for (const st of states) {
250
+ for (const t of st.topics) topics.set(t.key, t.label);
251
+ const ordinal = new Map();
252
+ for (const sec of st.sections) {
253
+ const n = ordinal.get(sec.topicKey) ?? 0;
254
+ ordinal.set(sec.topicKey, n + 1);
255
+ const k = `${sec.topicKey}#${n}`;
256
+ sections.set(k, [...(sections.get(k) ?? []), sec]);
257
+ }
258
+ if (st.view !== 'executive') {
259
+ for (const f of st.figures) figures.add(`${st.topics.map((t) => t.key).join('|')}::${f}`);
260
+ for (const e of st.equations) equations.add(`${st.topics.map((t) => t.key).join('|')}::${e}`);
261
+ }
262
+ if (route === '/introduction' && st.view === 'executive') {
263
+ if (st.viewEquations > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewEquations} equation(s); the executive view holds no equation`);
264
+ if (st.viewGlossaries > 0) bad(st.topics[0]?.label ?? '', `${NAME}, executive view (${L}): ${st.viewGlossaries} symbol glossary(ies); the executive view holds none`);
265
+ }
266
+ }
267
+ const seen = [...sections.values()].map(mergeSections);
268
+ const tname = (label) => (label ? `topic "${label}"` : 'the page');
269
+
270
+ if (expectedTopics > topics.size) bad('', `${NAME} (${L}): the walk visited ${topics.size} of ${expectedTopics} topics; the floors of an unvisited topic were never checked`);
271
+
272
+ // ---- the lede
273
+ if (!waived.has('lede-words') && first.ledeWords < LEDE_WORDS) bad('', `${NAME} (${L}): the lede holds ${first.ledeWords} word(s), the floor is ${LEDE_WORDS}`);
274
+
275
+ // ---- the Introduction's views: the executive one first
276
+ if (route === '/introduction') {
277
+ const v = first.views ?? [];
278
+ 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)`);
279
+ else if (v[0] !== 'executive') bad('', `${NAME} (${L}): the executive view is not the first topic (the first is "${v[0]}")`);
280
+ 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'}")`);
281
+ if (!v.includes('technical')) bad('', `${NAME} (${L}): no technical view; the top-level topics are [${v.join(', ') || 'none'}]`);
282
+ }
283
+
284
+ // ---- each section that declares an area
285
+ const present = new Set();
286
+ for (const sec of seen) {
287
+ if (route === '/introduction' && (first.views ?? []).length > 0 && sec.view !== 'executive' && sec.view !== 'technical') {
288
+ 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)`);
289
+ }
290
+ if (!sec.area) continue;
291
+ const def = P.areas[sec.area];
292
+ if (!def) {
293
+ 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(', ')})`);
294
+ continue;
295
+ }
296
+ present.add(sec.area);
297
+ if (waived.has(sec.area)) continue;
298
+ if (def.view && (def.view === 'executive') !== (sec.view === 'executive')) {
299
+ 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`);
300
+ }
301
+ 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 ')}`);
302
+ for (const t of failedRules(sec, def.requires)) bad(sec.topic, `${NAME}, ${sec.area} area, ${tname(sec.topic)} (${L}): ${t}`);
303
+ }
304
+
305
+ // ---- every area of the page is declared at least once
306
+ for (const [name, def] of Object.entries(P.areas)) {
307
+ if (waived.has(name) || present.has(name)) continue;
308
+ bad('', `${NAME} (${L}): no section declares the area "${name}" (${def.what.en})`);
309
+ }
310
+
311
+ // ---- Methodology: one method section per topic
312
+ if (route === '/methodology' && !waived.has('method')) {
313
+ for (const [key, label] of topics) {
314
+ const n = seen.filter((s) => s.topicKey === key && s.area === 'method').length;
315
+ if (n !== 1) bad(label, `${NAME}, ${tname(label)} (${L}): ${n} method section(s), exactly 1 is required`);
316
+ }
317
+ }
318
+
319
+ // ---- the page-level floors
320
+ const kinds = new Set(seen.filter((s) => s.area === 'method' && s.kind === 'learned').map((s) => s.topicKey));
321
+ const measured = { topics: topics.size, learned: kinds.size, figures: figures.size, equations: equations.size };
322
+ for (const [id, f] of Object.entries(P.floors)) {
323
+ if (waived.has(id)) continue;
324
+ if (measured[f.measure] < f.min) bad('', `${NAME} (${L}): ${measured[f.measure]} ${plural(f.noun, measured[f.measure])}, the floor is ${f.min}`);
325
+ }
326
+ return { failures, waivers, topics: topics.size };
327
+ }