@fasl-work/caos-app-shell 0.10.0 → 0.11.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/README.md +11 -2
- package/bin/caos-shell-gate.mjs +3 -1
- package/dist/chart.d.ts +19 -2
- package/dist/chart.js +50 -8
- package/dist/index.d.ts +91 -9
- package/dist/index.js +188 -71
- package/gate/areas.mjs +327 -0
- package/gate/inpage.mjs +338 -0
- package/gate/run.mjs +45 -7
- package/package.json +3 -2
- package/reserved-classes.json +18 -0
- package/styles.css +23 -3
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
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
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
|
+
}
|