@brianmsm/quarto-bento 0.0.0-stage → 0.1.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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -2
  3. package/bin/qbento-npm.mjs +62 -0
  4. package/distribution/LICENSE +21 -0
  5. package/distribution/PACKAGING.md +59 -0
  6. package/distribution/bin/qbento +5 -0
  7. package/distribution/bin/qbento.cmd +4 -0
  8. package/distribution/manifest.json +720 -0
  9. package/distribution/runtime/LICENSE +21 -0
  10. package/distribution/runtime/_extensions/qbento/_extension.yml +24 -0
  11. package/distribution/runtime/_extensions/qbento/capture.lua +21 -0
  12. package/distribution/runtime/_extensions/qbento/guard.lua +7 -0
  13. package/distribution/runtime/_extensions/qbento/post.ts +2 -0
  14. package/distribution/runtime/_extensions/qbento/pre.ts +2 -0
  15. package/distribution/runtime/_extensions/qbento/producer-activation.js +129 -0
  16. package/distribution/runtime/src/bridge/browser.js +168 -0
  17. package/distribution/runtime/src/bridge/isolation.ts +55 -0
  18. package/distribution/runtime/src/browser/chromium.ts +218 -0
  19. package/distribution/runtime/src/cli.ts +232 -0
  20. package/distribution/runtime/src/core/agent.ts +546 -0
  21. package/distribution/runtime/src/core/annotations.ts +384 -0
  22. package/distribution/runtime/src/core/capture.ts +203 -0
  23. package/distribution/runtime/src/core/component-actions.ts +550 -0
  24. package/distribution/runtime/src/core/component-frames.ts +59 -0
  25. package/distribution/runtime/src/core/component-sync.ts +290 -0
  26. package/distribution/runtime/src/core/design-diagnostics.ts +78 -0
  27. package/distribution/runtime/src/core/design-registry.ts +287 -0
  28. package/distribution/runtime/src/core/graph-relations.ts +354 -0
  29. package/distribution/runtime/src/core/graph-scope.ts +63 -0
  30. package/distribution/runtime/src/core/graph-sync.ts +228 -0
  31. package/distribution/runtime/src/core/io.ts +27 -0
  32. package/distribution/runtime/src/core/kpi-appearance.ts +205 -0
  33. package/distribution/runtime/src/core/layouts.ts +245 -0
  34. package/distribution/runtime/src/core/project.ts +851 -0
  35. package/distribution/runtime/src/core/reconcile.ts +384 -0
  36. package/distribution/runtime/src/core/recovery.ts +1255 -0
  37. package/distribution/runtime/src/core/slide-actions.ts +158 -0
  38. package/distribution/runtime/src/core/source-scope.ts +50 -0
  39. package/distribution/runtime/src/core/sync.ts +2557 -0
  40. package/distribution/runtime/src/core/widgets.ts +300 -0
  41. package/distribution/runtime/src/integration/install.ts +98 -0
  42. package/distribution/runtime/src/integration/invocation.ts +100 -0
  43. package/distribution/runtime/src/integration/quarto.ts +457 -0
  44. package/distribution/runtime/src/render/assets.ts +66 -0
  45. package/distribution/runtime/src/render/capture.lua +8 -0
  46. package/distribution/runtime/src/render/chart.ts +165 -0
  47. package/distribution/runtime/src/render/components.ts +199 -0
  48. package/distribution/runtime/src/render/design.ts +820 -0
  49. package/distribution/runtime/src/render/diagnostics.ts +36 -0
  50. package/distribution/runtime/src/render/graphs.ts +747 -0
  51. package/distribution/runtime/src/render/html.ts +82 -0
  52. package/distribution/runtime/src/render/images.ts +268 -0
  53. package/distribution/runtime/src/render/inline.ts +259 -0
  54. package/distribution/runtime/src/render/kpi.ts +385 -0
  55. package/distribution/runtime/src/render/lists.ts +183 -0
  56. package/distribution/runtime/src/render/math.ts +413 -0
  57. package/distribution/runtime/src/render/measure.ts +185 -0
  58. package/distribution/runtime/src/render/native-layout.ts +174 -0
  59. package/distribution/runtime/src/render/producer-capture.ts +319 -0
  60. package/distribution/runtime/src/render/producer-environment.R +49 -0
  61. package/distribution/runtime/src/render/producer-environment.ts +96 -0
  62. package/distribution/runtime/src/render/producer-profile.R +91 -0
  63. package/distribution/runtime/src/render/producer-site-profile.R +18 -0
  64. package/distribution/runtime/src/render/project.ts +1806 -0
  65. package/distribution/runtime/src/render/quarto.ts +127 -0
  66. package/distribution/runtime/src/render/registry.ts +233 -0
  67. package/distribution/runtime/src/render/resources.ts +292 -0
  68. package/distribution/runtime/src/render/shell.ts +110 -0
  69. package/distribution/runtime/src/render/source-inline.ts +169 -0
  70. package/distribution/runtime/src/render/table.ts +220 -0
  71. package/distribution/runtime/src/render/text-overflow.ts +208 -0
  72. package/distribution/runtime/src/render/types.ts +71 -0
  73. package/distribution/runtime/src/render/validate.ts +572 -0
  74. package/distribution/runtime/src/rich/project.ts +117 -0
  75. package/distribution/runtime/src/rich/table.ts +374 -0
  76. package/distribution/runtime/src/source/annotations.ts +490 -0
  77. package/distribution/runtime/src/source/attributes.ts +415 -0
  78. package/distribution/runtime/src/source/chart.ts +165 -0
  79. package/distribution/runtime/src/source/code-language.ts +277 -0
  80. package/distribution/runtime/src/source/component-insertion.ts +160 -0
  81. package/distribution/runtime/src/source/deletion.ts +278 -0
  82. package/distribution/runtime/src/source/graphs.ts +517 -0
  83. package/distribution/runtime/src/source/identities.ts +256 -0
  84. package/distribution/runtime/src/source/identity-id.ts +6 -0
  85. package/distribution/runtime/src/source/image.ts +50 -0
  86. package/distribution/runtime/src/source/kpi.ts +196 -0
  87. package/distribution/runtime/src/source/mod.ts +876 -0
  88. package/distribution/runtime/src/source/notes.ts +308 -0
  89. package/distribution/runtime/src/source/structure.ts +647 -0
  90. package/distribution/runtime/src/source/table.ts +290 -0
  91. package/distribution/runtime/src/source/text-conversion.ts +146 -0
  92. package/distribution/runtime/src/state/components.ts +281 -0
  93. package/distribution/runtime/src/state/graphs.ts +200 -0
  94. package/distribution/runtime/src/state/json.ts +162 -0
  95. package/distribution/runtime/src/state/merge.ts +1034 -0
  96. package/distribution/runtime/src/state/mod.ts +5 -0
  97. package/distribution/runtime/src/state/store.ts +226 -0
  98. package/distribution/runtime/src/state/types.ts +151 -0
  99. package/distribution/runtime/src/studio/client.js +892 -0
  100. package/distribution/runtime/src/studio/components.js +188 -0
  101. package/distribution/runtime/src/studio/index.html +283 -0
  102. package/distribution/runtime/src/studio/layouts.js +170 -0
  103. package/distribution/runtime/src/studio/registry.js +283 -0
  104. package/distribution/runtime/src/studio/relations.js +169 -0
  105. package/distribution/runtime/src/studio/server.ts +461 -0
  106. package/distribution/runtime/src/widgets/deck.ts +39 -0
  107. package/distribution/runtime/src/widgets/plotly.ts +354 -0
  108. package/distribution/runtime/src/widgets/presenter.ts +367 -0
  109. package/distribution/runtime/src/widgets/snapshot.ts +116 -0
  110. package/distribution/runtime/vendor/bento/FONT_NOTICES.md +49 -0
  111. package/distribution/runtime/vendor/bento/LICENSE +21 -0
  112. package/distribution/runtime/vendor/bento/THIRD_PARTY_NOTICES.md +51 -0
  113. package/distribution/runtime/vendor/bento/font-licenses/fraunces-OFL.txt +93 -0
  114. package/distribution/runtime/vendor/bento/font-licenses/instrument-sans-OFL.txt +93 -0
  115. package/distribution/runtime/vendor/bento/pin.json +69 -0
  116. package/distribution/runtime/vendor/bento/provenance/slides.package-lock.json +1928 -0
  117. package/distribution/runtime/vendor/bento/provenance/slides.package.json +28 -0
  118. package/distribution/runtime/vendor/bento/runtime-dependencies.json +244 -0
  119. package/distribution/runtime/vendor/bento/runtime-licenses.json +4038 -0
  120. package/distribution/runtime/vendor/bento/shell.html +269 -0
  121. package/distribution/runtime/vendor/bento/slides.schema.json +2379 -0
  122. package/package.json +40 -4
@@ -0,0 +1,256 @@
1
+ /** Inspectable identity acquisition; source writes belong only to approved sync. */
2
+ import { literalTableBlock } from "./table.ts";
3
+ import {
4
+ hashSource,
5
+ scanSource,
6
+ sliceBytes,
7
+ type SourceDocument,
8
+ SourceError,
9
+ type SourcePatch,
10
+ } from "./mod.ts";
11
+ import { isNativeIdentity, isSourceIdentity } from "./identity-id.ts";
12
+ import {
13
+ originalAttributeTokens,
14
+ originalPandocAttributes,
15
+ } from "./attributes.ts";
16
+
17
+ export interface IdentityRequest {
18
+ blockId: string;
19
+ sourceId: string;
20
+ nativeId: string;
21
+ imageNativeId?: string;
22
+ title: boolean;
23
+ }
24
+ export interface IdentityPreview {
25
+ version: 1;
26
+ kind: "source-identities";
27
+ sourceHash: string;
28
+ requests: IdentityRequest[];
29
+ /** Preserve offset-derived computed identities without editing executable bytes. */
30
+ producerRequests?: string[];
31
+ patches: SourcePatch[];
32
+ bindings: { blockId: string; newBlockId: string }[];
33
+ result: string;
34
+ resultHash: string;
35
+ }
36
+ const encoder = new TextEncoder(),
37
+ decoder = new TextDecoder("utf-8", { fatal: true });
38
+ function fail(message: string): never {
39
+ throw new SourceError("ambiguous-identity", message);
40
+ }
41
+ function splice(text: string, patches: SourcePatch[]) {
42
+ let bytes = encoder.encode(text);
43
+ for (
44
+ const patch of [...patches].sort((a, b) => b.span.start - a.span.start)
45
+ ) {
46
+ if (sliceBytes(text, patch.span) !== patch.expected) {
47
+ fail(
48
+ "Identity fragment changed.",
49
+ );
50
+ }
51
+ const replacement = encoder.encode(patch.replacement),
52
+ next = new Uint8Array(
53
+ bytes.length - (patch.span.end - patch.span.start) + replacement.length,
54
+ );
55
+ next.set(bytes.subarray(0, patch.span.start));
56
+ next.set(replacement, patch.span.start);
57
+ next.set(
58
+ bytes.subarray(patch.span.end),
59
+ patch.span.start + replacement.length,
60
+ );
61
+ bytes = next;
62
+ }
63
+ return decoder.decode(bytes);
64
+ }
65
+ export function previewIdentities(
66
+ document: SourceDocument,
67
+ requests: IdentityRequest[],
68
+ producerRequests: string[] = [],
69
+ ): IdentityPreview {
70
+ if (
71
+ hashSource(document.text) !== document.hash || document.diagnostics.length
72
+ ) fail("Resolve source diagnostics before acquiring identities.");
73
+ const patches: SourcePatch[] = [],
74
+ bindings: IdentityPreview["bindings"] = [],
75
+ reserved = new Set(
76
+ document.blocks.filter((b) => b.stable).map((b) => b.id),
77
+ );
78
+ const seen = new Set<string>();
79
+ for (const request of requests) {
80
+ if (
81
+ !isSourceIdentity(request.sourceId) ||
82
+ !isNativeIdentity(request.nativeId) ||
83
+ (request.imageNativeId !== undefined &&
84
+ (!isNativeIdentity(request.imageNativeId) ||
85
+ request.imageNativeId === request.nativeId)) ||
86
+ seen.has(request.blockId) ||
87
+ reserved.has(request.sourceId)
88
+ ) fail("Source/native identities are unsafe or collide.");
89
+ seen.add(request.blockId);
90
+ reserved.add(request.sourceId);
91
+ const blocks = document.blocks.filter((block) =>
92
+ block.id === request.blockId
93
+ );
94
+ if (blocks.length !== 1) {
95
+ fail("Identity does not select exactly one original block.");
96
+ }
97
+ const block = blocks[0];
98
+ const table = block.kind === "unsupported" &&
99
+ literalTableBlock(document, block.id);
100
+ if (
101
+ block.stable || block.origin !== "authored" ||
102
+ !block.editable && !table ||
103
+ !table &&
104
+ !["heading", "paragraph", "list", "math", "code", "image"].includes(
105
+ block.kind,
106
+ )
107
+ ) {
108
+ fail(
109
+ "Only weak authored headings, paragraphs, lists, display equations and displayed code can acquire a verified identity.",
110
+ );
111
+ }
112
+ if (block.parentId) {
113
+ const parent = document.blocks.find((candidate) =>
114
+ candidate.id === block.parentId
115
+ );
116
+ if (!parent || /\.bento-[\w-]+/.test(parent.raw.split(/\r?\n/)[0])) {
117
+ fail(
118
+ "Pin component child identities explicitly; wrapping must not change component structure.",
119
+ );
120
+ }
121
+ }
122
+ let replacement: string, newBlockId: string;
123
+ if (block.kind === "heading") {
124
+ const newline = block.raw.endsWith("\r\n")
125
+ ? "\r\n"
126
+ : block.raw.endsWith("\n")
127
+ ? "\n"
128
+ : "";
129
+ const line = block.raw.slice(0, block.raw.length - newline.length),
130
+ attrs = /\{([^{}]*)\}[ \t]*$/.exec(line);
131
+ const native = request.title
132
+ ? ` bento-title-id="${request.nativeId}"`
133
+ : "";
134
+ if (attrs) {
135
+ const attr = originalPandocAttributes(
136
+ attrs[1],
137
+ originalAttributeTokens(attrs[1]),
138
+ );
139
+ if (attr[0] || attr[2].some(([name]) => name === "bento-title-id")) {
140
+ fail("Heading already has an ambiguous source/native declaration.");
141
+ }
142
+ replacement = line.slice(0, attrs.index) +
143
+ `{#${request.sourceId} ${attrs[1]}${native}}` +
144
+ line.slice(attrs.index + attrs[0].length) + newline;
145
+ } else replacement = line + ` {#${request.sourceId}${native}}` + newline;
146
+ newBlockId = request.sourceId;
147
+ } else {
148
+ if (request.title) fail("A body text block is not a slide title.");
149
+ if (
150
+ block.kind === "image" && !request.imageNativeId ||
151
+ block.kind !== "image" && request.imageNativeId !== undefined
152
+ ) fail("Image identity needs its original checked native pair.");
153
+ const newline = document.newline;
154
+ const body = block.raw + (block.raw.endsWith("\n") ? "" : newline);
155
+ replacement =
156
+ `::: {#${request.sourceId} bento-element-id="${request.nativeId}"${
157
+ request.imageNativeId
158
+ ? ` bento-image-id="${request.imageNativeId}"`
159
+ : ""
160
+ }}${newline}${newline}${body}${newline}:::${newline}`;
161
+ newBlockId = `${request.sourceId}/${block.kind}:1`;
162
+ }
163
+ patches.push({
164
+ blockId: block.id,
165
+ span: block.span,
166
+ expected: block.raw,
167
+ replacement,
168
+ });
169
+ bindings.push({ blockId: block.id, newBlockId });
170
+ }
171
+ for (const producerId of producerRequests) {
172
+ const blocks = document.blocks.filter((block) => block.id === producerId);
173
+ const block = blocks[0];
174
+ if (
175
+ !/^chunk@(?:0|[1-9][0-9]*)$/.test(producerId) || seen.has(producerId) ||
176
+ blocks.length !== 1 || !block || block.kind !== "chunk" ||
177
+ block.origin !== "computed" || block.stable || block.parentId ||
178
+ block.id !== `chunk@${block.span.start}`
179
+ ) {
180
+ fail(
181
+ "Producer identity needs one exact unlabelled top-level executable chunk.",
182
+ );
183
+ }
184
+ seen.add(producerId);
185
+ patches.push({
186
+ blockId: producerId,
187
+ span: { start: block.span.start, end: block.span.start },
188
+ expected: "",
189
+ replacement: `<!-- qb-producer:${producerId} -->${document.newline}`,
190
+ });
191
+ }
192
+ const ordered = [...patches].sort((a, b) => a.span.start - b.span.start);
193
+ for (let i = 1; i < ordered.length; i++) {
194
+ if (ordered[i].span.start < ordered[i - 1].span.end) {
195
+ fail("Identity patches overlap.");
196
+ }
197
+ }
198
+ const result = splice(document.text, patches), fresh = scanSource(result);
199
+ if (fresh.diagnostics.length) {
200
+ fail("Acquired identities make source structure ambiguous.");
201
+ }
202
+ for (const binding of bindings) {
203
+ const before = document.blocks.find((block) =>
204
+ block.id === binding.blockId
205
+ )!,
206
+ after = fresh.blocks.filter((block) => block.id === binding.newBlockId);
207
+ if (
208
+ after.length !== 1 || after[0].kind !== before.kind ||
209
+ after[0].content !== before.content || after[0].origin !== "authored"
210
+ ) fail("Identity acquisition changed authored content.");
211
+ }
212
+ for (const producerId of producerRequests) {
213
+ const before = document.blocks.find((block) => block.id === producerId)!;
214
+ const matches = fresh.blocks.filter((block) => block.id === producerId);
215
+ const after = matches[0];
216
+ if (
217
+ matches.length !== 1 || after.kind !== "chunk" ||
218
+ after.origin !== "computed" || !after.stable ||
219
+ after.raw !== before.raw || after.content !== before.content ||
220
+ after.editable
221
+ ) fail("Producer identity changed executable content or ownership.");
222
+ }
223
+ return {
224
+ version: 1,
225
+ kind: "source-identities",
226
+ sourceHash: document.hash,
227
+ requests: structuredClone(requests),
228
+ ...(producerRequests.length
229
+ ? { producerRequests: [...producerRequests] }
230
+ : {}),
231
+ patches,
232
+ bindings,
233
+ result,
234
+ resultHash: hashSource(result),
235
+ };
236
+ }
237
+ export function applyIdentities(text: string, plan: IdentityPreview): string {
238
+ if (hashSource(text) !== plan.sourceHash) {
239
+ throw new SourceError(
240
+ "stale-source",
241
+ "Source changed after identity preview.",
242
+ );
243
+ }
244
+ const fresh = previewIdentities(
245
+ scanSource(text),
246
+ plan.requests,
247
+ plan.producerRequests,
248
+ );
249
+ if (JSON.stringify(fresh) !== JSON.stringify(plan)) {
250
+ throw new SourceError(
251
+ "invalid-identity-plan",
252
+ "Identity preview was modified.",
253
+ );
254
+ }
255
+ return fresh.result;
256
+ }
@@ -0,0 +1,6 @@
1
+ /** Source identifiers are global; native identifiers are scoped to a slide. */
2
+ export const isSourceIdentity = (value: string): boolean =>
3
+ /^[\p{L}_][\p{L}\p{N}\p{M}_.:-]*$/u.test(value);
4
+ /** Existing mapped children can contain '/', and Unicode automatic heading IDs. */
5
+ export const isNativeIdentity = (value: string): boolean =>
6
+ /^[\p{L}_][\p{L}\p{N}\p{M}_.:/-]*$/u.test(value);
@@ -0,0 +1,50 @@
1
+ /** Exact Markdown caption span for one standalone inline image. The Pandoc
2
+ * proof remains responsible for confirming that the entire block is an image.
3
+ */
4
+ export const IMAGE_CODEC_VERSION = 1;
5
+ export interface ImageCaptionSyntax {
6
+ start: number;
7
+ end: number;
8
+ caption: string;
9
+ }
10
+ export function imageCaptionSyntax(
11
+ raw: string,
12
+ ): ImageCaptionSyntax | undefined {
13
+ const opening = /^ {0,3}!\[/.exec(raw);
14
+ if (!opening) return undefined;
15
+ const start = opening[0].length;
16
+ let depth = 1;
17
+ for (let index = start; index < raw.length; index++) {
18
+ const character = raw[index];
19
+ if (character === "\\") {
20
+ index++;
21
+ continue;
22
+ }
23
+ if (character === "`") {
24
+ const run = /^`+/.exec(raw.slice(index))![0];
25
+ let close = index + run.length;
26
+ while (close < raw.length) {
27
+ const next = raw.indexOf("`", close);
28
+ if (next < 0) return undefined;
29
+ const candidate = /^`+/.exec(raw.slice(next))![0];
30
+ if (candidate.length === run.length) {
31
+ index = next + run.length - 1;
32
+ break;
33
+ }
34
+ close = next + candidate.length;
35
+ }
36
+ if (close >= raw.length) return undefined;
37
+ continue;
38
+ }
39
+ if (character === "[") depth++;
40
+ if (character === "]" && --depth === 0) {
41
+ if (raw[index + 1] !== "(") return undefined;
42
+ return {
43
+ start: new TextEncoder().encode(raw.slice(0, start)).length,
44
+ end: new TextEncoder().encode(raw.slice(0, index)).length,
45
+ caption: raw.slice(start, index),
46
+ };
47
+ }
48
+ }
49
+ return undefined;
50
+ }
@@ -0,0 +1,196 @@
1
+ /** Original-source contract for the deliberately bounded KPI composition. */
2
+ import {
3
+ hashSource,
4
+ scanSource,
5
+ type SourceBlock,
6
+ type SourceDocument,
7
+ } from "./mod.ts";
8
+ import {
9
+ originalAttributeTokens,
10
+ originalPandocAttributes,
11
+ } from "./attributes.ts";
12
+ import {
13
+ type DesignDeclaration,
14
+ DesignError,
15
+ nativeDesignField,
16
+ type PandocAttr,
17
+ parseDesignAttributes,
18
+ } from "../render/design.ts";
19
+
20
+ export type KpiRole = "label" | "value" | "detail";
21
+ export interface KpiSlot {
22
+ role: KpiRole;
23
+ wrapper: SourceBlock;
24
+ paragraph: SourceBlock;
25
+ attr: PandocAttr;
26
+ design: DesignDeclaration;
27
+ }
28
+ export interface KpiSource {
29
+ wrapper: SourceBlock;
30
+ attr: PandocAttr;
31
+ design: DesignDeclaration;
32
+ slots: KpiSlot[];
33
+ /** Verified outermost source component; native Bento grouping is flat. */
34
+ groupId: string;
35
+ ancestors: string[];
36
+ }
37
+ function fail(code: string, message: string): never {
38
+ throw new DesignError(code, message);
39
+ }
40
+ export function kpiSourceAttributes(block: SourceBlock): PandocAttr {
41
+ const content = /^ {0,3}:{3,}\s*\{([^{}]*)\}\s*$/.exec(
42
+ block.raw.split(/\r?\n/)[0],
43
+ )?.[1];
44
+ if (content === undefined) {
45
+ fail(
46
+ "KPI_SOURCE_ATTRIBUTES",
47
+ "Use literal fenced-div attributes for KPI identities and roles.",
48
+ );
49
+ }
50
+ return originalPandocAttributes(content, originalAttributeTokens(content));
51
+ }
52
+ /** A computed paragraph is its own inline source proof; no chunk is invented. */
53
+ function assertInlineValue(block: SourceBlock): void {
54
+ if (block.origin !== "computed") return;
55
+ // Quarto's ordinary inline forms only. Shortcodes, multiline/delimiter tricks,
56
+ // and unproved dynamic constructs have no faithful KPI mapping in this slice.
57
+ if (/\{\{[<%]|\r|\n/.test(block.content)) {
58
+ fail(
59
+ "KPI_INLINE_SOURCE_UNSUPPORTED",
60
+ "Use a single-line Quarto inline expression in the value slot.",
61
+ );
62
+ }
63
+ const codes = [...block.content.matchAll(/(?<!`)(`+)([^`]*?)\1(?!`)/g)];
64
+ const expressions = codes.filter((match) =>
65
+ /^(?:r\s+|\{(?:r|python|julia)\}\s*)\S/.test(match[2])
66
+ );
67
+ if (!expressions.length || expressions.some((match) => match[1] !== "`")) {
68
+ fail(
69
+ "KPI_INLINE_SOURCE_UNSUPPORTED",
70
+ "The computed value needs an explicit supported Quarto inline expression, not a synthetic producer.",
71
+ );
72
+ }
73
+ }
74
+ export function kpiSource(document: SourceDocument, id: string): KpiSource {
75
+ if (document.hash !== hashSource(document.text)) {
76
+ fail(
77
+ "KPI_SOURCE_CHANGED",
78
+ "The original KPI source snapshot hash is stale.",
79
+ );
80
+ }
81
+ const source = scanSource(document.text);
82
+ if (source.diagnostics.length) {
83
+ fail(
84
+ "KPI_SOURCE_IDENTITY",
85
+ "Resolve duplicate IDs or malformed source structure before rendering a KPI.",
86
+ );
87
+ }
88
+ const wrappers = source.blocks.filter((block) => block.id === id);
89
+ if (
90
+ wrappers.length !== 1 || wrappers[0].kind !== "div" ||
91
+ !wrappers[0].explicitId
92
+ ) {
93
+ fail(
94
+ "KPI_SOURCE_IDENTITY",
95
+ "A KPI needs one globally identified original fenced div.",
96
+ );
97
+ }
98
+ const wrapper = wrappers[0], attr = kpiSourceAttributes(wrapper);
99
+ const design = parseDesignAttributes(attr);
100
+ if (attr[0] !== id || design.component !== "kpi" || id.length + 18 > 160) {
101
+ fail(
102
+ "KPI_SOURCE_IDENTITY",
103
+ "The original .bento-kpi identity must fit the pinned native object IDs.",
104
+ );
105
+ }
106
+ const ancestors: string[] = [];
107
+ let parentId = wrapper.parentId;
108
+ while (parentId) {
109
+ const parent = source.blocks.find((block) => block.id === parentId);
110
+ if (
111
+ !parent || ancestors.includes(parentId) || parent.kind !== "div" ||
112
+ !parent.explicitId || parent.origin !== "authored" ||
113
+ parent.slideId !== wrapper.slideId ||
114
+ !["card", "layout"].includes(
115
+ parseDesignAttributes(kpiSourceAttributes(parent)).component ?? "",
116
+ )
117
+ ) {
118
+ fail(
119
+ "KPI_SOURCE_CONTEXT",
120
+ "KPI ancestors must be identified original cards/layouts on the same slide, outside execution output.",
121
+ );
122
+ }
123
+ ancestors.push(parentId);
124
+ parentId = parent.parentId;
125
+ }
126
+ const children = source.blocks.filter((block) => block.parentId === id);
127
+ if (
128
+ children.length < 2 || children.length > 3 ||
129
+ children.some((block) => block.kind !== "div" || !block.explicitId)
130
+ ) {
131
+ fail(
132
+ "KPI_SLOT_STRUCTURE",
133
+ "A KPI needs exactly two or three identified direct div children: label, value, optional detail.",
134
+ );
135
+ }
136
+ const roles: KpiRole[] = ["label", "value", "detail"];
137
+ const slots = children.map((child, index): KpiSlot => {
138
+ const childAttr = kpiSourceAttributes(child);
139
+ const declared = childAttr[2].filter(([key]) => key === "bento-role");
140
+ if (
141
+ childAttr[0] !== child.id || child.id.length + 12 > 160 ||
142
+ declared.length !== 1 || declared[0][1] !== roles[index] ||
143
+ childAttr[1].length
144
+ ) {
145
+ fail(
146
+ "KPI_SLOT_ROLE",
147
+ "Declare the unique roles label, value, optional detail in that order, on plain identified div children.",
148
+ );
149
+ }
150
+ const descendants = source.blocks.filter((block) =>
151
+ block.parentId === child.id
152
+ );
153
+ if (
154
+ descendants.length !== 1 || descendants[0].kind !== "paragraph" ||
155
+ descendants[0].id !== `${child.id}/paragraph:1`
156
+ ) {
157
+ fail(
158
+ "KPI_SLOT_PARAGRAPH",
159
+ "Each KPI role needs exactly one ordinary paragraph; nested blocks and separately marked paragraphs refuse.",
160
+ );
161
+ }
162
+ if (
163
+ childAttr[2].some(([key]) =>
164
+ key !== "bento-role" && !nativeDesignField(key)
165
+ )
166
+ ) {
167
+ fail(
168
+ "KPI_SLOT_ATTRIBUTE",
169
+ "KPI children accept bento-role and supported native text design fields only.",
170
+ );
171
+ }
172
+ const paragraph = descendants[0];
173
+ if (paragraph.origin === "computed" && roles[index] !== "value") {
174
+ fail(
175
+ "KPI_LITERAL_ROLE_REQUIRED",
176
+ "Label and detail are authored literal paragraphs; inline computation belongs to value.",
177
+ );
178
+ }
179
+ assertInlineValue(paragraph);
180
+ return {
181
+ role: roles[index],
182
+ wrapper: child,
183
+ paragraph,
184
+ attr: childAttr,
185
+ design: parseDesignAttributes(childAttr),
186
+ };
187
+ });
188
+ return {
189
+ wrapper,
190
+ attr,
191
+ design,
192
+ slots,
193
+ ancestors,
194
+ groupId: ancestors.at(-1) ?? id,
195
+ };
196
+ }