@inflexa-ai/harness 0.20.0 → 0.21.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 (133) hide show
  1. package/dist/agents/conversation-agent.d.ts +26 -14
  2. package/dist/agents/conversation-agent.js +12 -23
  3. package/dist/agents/report-session-agent.d.ts +31 -5
  4. package/dist/agents/report-session-agent.js +35 -9
  5. package/dist/app/chat-turn.js +3 -1
  6. package/dist/app/message-assembly.d.ts +15 -1
  7. package/dist/app/message-assembly.js +22 -8
  8. package/dist/app/report-session-runtime.d.ts +21 -4
  9. package/dist/app/report-session-runtime.js +53 -6
  10. package/dist/app/spawn-report-session.d.ts +174 -17
  11. package/dist/app/spawn-report-session.js +233 -21
  12. package/dist/contracts/chat-events.d.ts +7 -2
  13. package/dist/contracts/message.d.ts +10 -0
  14. package/dist/contracts/report-blocks.d.ts +743 -19
  15. package/dist/contracts/report-blocks.js +186 -11
  16. package/dist/contracts/report-reference.d.ts +88 -0
  17. package/dist/contracts/report-reference.js +48 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +20 -0
  20. package/dist/lib/chrome.d.ts +26 -6
  21. package/dist/lib/chrome.js +99 -32
  22. package/dist/lib/eyes.d.ts +57 -0
  23. package/dist/lib/eyes.js +45 -0
  24. package/dist/lib/page-capture.d.ts +14 -0
  25. package/dist/lib/page-capture.js +56 -2
  26. package/dist/loop/run-agent.js +58 -23
  27. package/dist/loop/tool-detail.d.ts +19 -2
  28. package/dist/loop/tool-detail.js +35 -2
  29. package/dist/memory/content-to-cortex.d.ts +3 -2
  30. package/dist/memory/content-to-cortex.js +21 -12
  31. package/dist/memory/conversation-display-replay.js +14 -7
  32. package/dist/memory/thread-history.d.ts +60 -3
  33. package/dist/memory/thread-history.js +55 -16
  34. package/dist/memory/thread-store.d.ts +11 -1
  35. package/dist/memory/thread-store.js +12 -2
  36. package/dist/prompts/conversation.d.ts +1 -1
  37. package/dist/prompts/conversation.js +23 -6
  38. package/dist/prompts/report-session.d.ts +1 -1
  39. package/dist/prompts/report-session.js +190 -9
  40. package/dist/report-model/authoring-grammar.d.ts +463 -0
  41. package/dist/report-model/authoring-grammar.js +77 -0
  42. package/dist/report-model/block-walk.d.ts +57 -14
  43. package/dist/report-model/block-walk.js +116 -11
  44. package/dist/report-model/draft-finish.d.ts +22 -4
  45. package/dist/report-model/draft-finish.js +39 -7
  46. package/dist/report-model/draft-operations.d.ts +3 -0
  47. package/dist/report-model/draft-operations.js +19 -5
  48. package/dist/report-model/draft-read.d.ts +8 -5
  49. package/dist/report-model/draft-read.js +21 -5
  50. package/dist/report-model/fixture-resolver.js +15 -4
  51. package/dist/report-model/pin-snapshot.d.ts +24 -6
  52. package/dist/report-model/pin-snapshot.js +204 -6
  53. package/dist/report-model/production-resolver.js +15 -4
  54. package/dist/report-model/reference-resolver.d.ts +50 -1
  55. package/dist/report-model/reference-resolver.js +94 -1
  56. package/dist/report-model/reference-stamp.d.ts +45 -0
  57. package/dist/report-model/reference-stamp.js +173 -0
  58. package/dist/report-model/structural-validation.d.ts +4 -1
  59. package/dist/report-model/structural-validation.js +48 -3
  60. package/dist/report-model/validate.d.ts +1 -1
  61. package/dist/report-model/validate.js +1 -1
  62. package/dist/report-render/asset-lookup.d.ts +19 -0
  63. package/dist/report-render/asset-lookup.js +37 -0
  64. package/dist/report-render/assets.d.ts +45 -1
  65. package/dist/report-render/assets.js +65 -6
  66. package/dist/report-render/chart-presets.d.ts +102 -0
  67. package/dist/report-render/chart-presets.js +205 -0
  68. package/dist/report-render/chart.d.ts +112 -10
  69. package/dist/report-render/chart.js +1164 -41
  70. package/dist/report-render/design.d.ts +99 -0
  71. package/dist/report-render/design.js +289 -107
  72. package/dist/report-render/number-format.d.ts +117 -0
  73. package/dist/report-render/number-format.js +479 -0
  74. package/dist/report-render/page.d.ts +121 -5
  75. package/dist/report-render/page.js +689 -7
  76. package/dist/report-render/references.d.ts +70 -16
  77. package/dist/report-render/references.js +54 -22
  78. package/dist/report-render/render.d.ts +23 -5
  79. package/dist/report-render/render.js +81 -22
  80. package/dist/report-render/table-data.d.ts +92 -0
  81. package/dist/report-render/table-data.js +129 -0
  82. package/dist/report-render/types.d.ts +15 -0
  83. package/dist/report-render/value-bridge.js +6 -2
  84. package/dist/report-render/views/chart-view.d.ts +12 -5
  85. package/dist/report-render/views/chart-view.js +14 -8
  86. package/dist/report-render/views/page-view.d.ts +36 -15
  87. package/dist/report-render/views/page-view.js +45 -23
  88. package/dist/report-render/views/prose.d.ts +12 -2
  89. package/dist/report-render/views/prose.js +29 -7
  90. package/dist/report-render/views/references-view.d.ts +22 -8
  91. package/dist/report-render/views/references-view.js +101 -13
  92. package/dist/report-render/views/values.d.ts +128 -13
  93. package/dist/report-render/views/values.js +199 -18
  94. package/dist/runtime/assemble.d.ts +39 -5
  95. package/dist/runtime/assemble.js +47 -6
  96. package/dist/sandbox/client.d.ts +10 -1
  97. package/dist/sandbox/create-sandbox.d.ts +9 -4
  98. package/dist/sandbox/create-sandbox.js +22 -16
  99. package/dist/sandbox/docker-client.js +10 -7
  100. package/dist/sandbox/mount-plan.d.ts +45 -7
  101. package/dist/sandbox/mount-plan.js +43 -5
  102. package/dist/sandbox/types.d.ts +6 -0
  103. package/dist/state/init.js +38 -9
  104. package/dist/state/report-session-state.d.ts +42 -1
  105. package/dist/state/report-session-state.js +70 -2
  106. package/dist/state/report-versions.d.ts +22 -17
  107. package/dist/state/report-versions.js +48 -35
  108. package/dist/state/snapshot-parse.js +16 -1
  109. package/dist/tools/define-tool.d.ts +32 -0
  110. package/dist/tools/define-tool.js +4 -0
  111. package/dist/tools/detail-resolver.d.ts +7 -2
  112. package/dist/tools/detail-resolver.js +7 -2
  113. package/dist/tools/iterate-report.d.ts +1 -1
  114. package/dist/tools/report/preview-snapshot.js +1 -0
  115. package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
  116. package/dist/tools/report-authoring/authoring-tools.js +99 -18
  117. package/dist/tools/report-session/derive-table.d.ts +162 -0
  118. package/dist/tools/report-session/derive-table.js +459 -0
  119. package/dist/tools/report-session/examine-page.d.ts +48 -18
  120. package/dist/tools/report-session/examine-page.js +204 -24
  121. package/dist/tools/report-session/index.d.ts +2 -0
  122. package/dist/tools/report-session/index.js +2 -0
  123. package/dist/tools/report-session/list-artifacts.d.ts +97 -0
  124. package/dist/tools/report-session/list-artifacts.js +197 -0
  125. package/dist/tools/report-session/preview-report.d.ts +6 -0
  126. package/dist/tools/report-session/preview-report.js +180 -28
  127. package/dist/tools/report-session/record-version.d.ts +22 -10
  128. package/dist/tools/report-session/record-version.js +99 -14
  129. package/dist/tools/start-report-session.d.ts +115 -0
  130. package/dist/tools/start-report-session.js +179 -0
  131. package/dist/workspace/paths.d.ts +50 -0
  132. package/dist/workspace/paths.js +63 -0
  133. package/package.json +2 -1
@@ -2,31 +2,85 @@
2
2
  * The reference ledger state.
3
3
  *
4
4
  * A claim binds to evidence, and a citation block points at an external source. The ledger collects each
5
- * reference in first-appearance order, and it gives one marker number to each distinct reference. A claim
6
- * marker and a citation marker read from the same ledger, thus one reference that two blocks share gets
7
- * one number and one list entry.
5
+ * reference in first-appearance order, and it gives one bracket number to each distinct reference. Every
6
+ * marker of the page reads from this one ladder, thus a reference that two blocks share gets one number and
7
+ * one appendix entry.
8
8
  *
9
- * Identity is the canonical serialization of the reference. `serializeReference` sorts the keys at every
10
- * depth, thus two references match only when every field matches. The key order of the source object does
11
- * not change the identity.
9
+ * One ladder holds both kinds. A reader of a report follows one notation, and the appendix lists flat in
10
+ * number order. Two ladders give one page two notations, and a reader must then learn which marker points
11
+ * at which list.
12
+ *
13
+ * The identity of a reference depends on its kind. An artifact identity is the canonical serialization of
14
+ * the reference: `serializeReference` sorts the keys at every depth, thus two references match only when
15
+ * every field matches, and two locators of one file stay apart. A citation identity is the citation key
16
+ * alone, because the key names the paper and the display text is authored beside it.
17
+ *
18
+ * The two identity forms cannot collide. A serialization is a JSON object, and a citation key is the
19
+ * prefixed `idKind:id` text. Thus one map holds both.
20
+ */
21
+ import { type CitationReference, type Reference } from "../contracts/report-reference.js";
22
+ /** The citation key of one citation reference, in the prefixed `idKind:id` form. */
23
+ export declare function citationKeyOf(reference: CitationReference): string;
24
+ /** One source of a derivation: the pinned path that the script read, and the content hash of those bytes. */
25
+ export interface DerivationChainSource {
26
+ readonly path: string;
27
+ readonly hash: string;
28
+ }
29
+ /**
30
+ * The chain of one derived path: the sources that the script read, and the hash of the script itself.
31
+ *
32
+ * The renderer declares the shape that the appendix reads, and never the durable record that carries it.
33
+ * Thus a caller passes the stored records straight through, and the render stays a pure function of plain
34
+ * data.
35
+ *
36
+ * The two sources are the links of the chain, and both are optional. `scriptSource` is the relative source
37
+ * of the staged script asset, and `outputSource` is the relative source of the derived file. The caller
38
+ * stages the script and it knows where the derived file sits, thus the caller states both and the renderer
39
+ * reads no disk. A chain that carries neither renders its hashes alone.
40
+ */
41
+ export interface DerivationChain {
42
+ readonly outputPath: string;
43
+ readonly sources: readonly DerivationChainSource[];
44
+ readonly scriptHash: string;
45
+ readonly scriptSource?: string;
46
+ readonly outputSource?: string;
47
+ }
48
+ /** The chain of each derived path of one render, keyed by the output path that the bindings name. */
49
+ export type DerivationChains = ReadonlyMap<string, DerivationChain>;
50
+ /**
51
+ * Key each chain by its output path.
52
+ *
53
+ * The appendix reads one chain for one path, thus the map is the read form of the list. A repeated output
54
+ * path keeps the first record, because the durable list refuses a repeated path and a second one carries no
55
+ * new meaning.
56
+ */
57
+ export declare function derivationChains(records: readonly DerivationChain[] | undefined): DerivationChains;
58
+ /**
59
+ * Each reference kind that names data, which is every kind except a citation.
60
+ *
61
+ * A derivation joins this set, because it computes over pinned artifacts and it names no paper.
12
62
  */
13
- import { type Reference } from "../contracts/report-reference.js";
63
+ export type ArtifactReference = Exclude<Reference, CitationReference>;
14
64
  /**
15
65
  * The mutable ledger of references.
16
66
  *
17
- * The page walk makes one ledger, and it threads the ledger through each claim and each citation. The
18
- * order of the entries is the order of the first mark, thus the marker numbers count up by first
19
- * appearance.
67
+ * The page walk makes one ledger, and it threads the ledger through each claim, each card, and each
68
+ * citation. The order of the entries is the order of the first mark, thus the marker numbers count up by
69
+ * first appearance across the whole page.
20
70
  */
21
71
  export declare class ReferenceLedger {
22
- private readonly order;
23
- private readonly markers;
72
+ private readonly references;
73
+ private readonly marks;
24
74
  /**
25
- * Give the marker number of a reference. A new reference gets the next number, and it joins the
26
- * order. A reference that matches an earlier one gives the earlier number, thus the list holds it one
27
- * time.
75
+ * Give the marker number of a reference. A new reference gets the next number of the ladder, and it
76
+ * joins the order of the ladder. A reference that matches an earlier one gives the earlier number, thus
77
+ * the appendix holds it one time.
78
+ *
79
+ * A citation matches on its key alone. The `raw` text and the display fields of a citation are the
80
+ * words of the author, and two blocks over one paper carry different words. The key names the paper,
81
+ * thus one paper takes one number and the appendix holds it one time.
28
82
  */
29
83
  mark(reference: Reference): number;
30
- /** The references in first-appearance order. The marker of an entry is its position plus one. */
84
+ /** Every reference in first-appearance order. The marker of an entry is its position plus one. */
31
85
  entries(): readonly Reference[];
32
86
  }
@@ -2,43 +2,75 @@
2
2
  * The reference ledger state.
3
3
  *
4
4
  * A claim binds to evidence, and a citation block points at an external source. The ledger collects each
5
- * reference in first-appearance order, and it gives one marker number to each distinct reference. A claim
6
- * marker and a citation marker read from the same ledger, thus one reference that two blocks share gets
7
- * one number and one list entry.
5
+ * reference in first-appearance order, and it gives one bracket number to each distinct reference. Every
6
+ * marker of the page reads from this one ladder, thus a reference that two blocks share gets one number and
7
+ * one appendix entry.
8
8
  *
9
- * Identity is the canonical serialization of the reference. `serializeReference` sorts the keys at every
10
- * depth, thus two references match only when every field matches. The key order of the source object does
11
- * not change the identity.
9
+ * One ladder holds both kinds. A reader of a report follows one notation, and the appendix lists flat in
10
+ * number order. Two ladders give one page two notations, and a reader must then learn which marker points
11
+ * at which list.
12
+ *
13
+ * The identity of a reference depends on its kind. An artifact identity is the canonical serialization of
14
+ * the reference: `serializeReference` sorts the keys at every depth, thus two references match only when
15
+ * every field matches, and two locators of one file stay apart. A citation identity is the citation key
16
+ * alone, because the key names the paper and the display text is authored beside it.
17
+ *
18
+ * The two identity forms cannot collide. A serialization is a JSON object, and a citation key is the
19
+ * prefixed `idKind:id` text. Thus one map holds both.
12
20
  */
13
21
  import { serializeReference } from "../contracts/report-reference.js";
22
+ /** The citation key of one citation reference, in the prefixed `idKind:id` form. */
23
+ export function citationKeyOf(reference) {
24
+ return `${reference.idKind}:${reference.id}`;
25
+ }
26
+ /**
27
+ * Key each chain by its output path.
28
+ *
29
+ * The appendix reads one chain for one path, thus the map is the read form of the list. A repeated output
30
+ * path keeps the first record, because the durable list refuses a repeated path and a second one carries no
31
+ * new meaning.
32
+ */
33
+ export function derivationChains(records) {
34
+ const chains = new Map();
35
+ for (const record of records ?? []) {
36
+ if (!chains.has(record.outputPath)) {
37
+ chains.set(record.outputPath, record);
38
+ }
39
+ }
40
+ return chains;
41
+ }
14
42
  /**
15
43
  * The mutable ledger of references.
16
44
  *
17
- * The page walk makes one ledger, and it threads the ledger through each claim and each citation. The
18
- * order of the entries is the order of the first mark, thus the marker numbers count up by first
19
- * appearance.
45
+ * The page walk makes one ledger, and it threads the ledger through each claim, each card, and each
46
+ * citation. The order of the entries is the order of the first mark, thus the marker numbers count up by
47
+ * first appearance across the whole page.
20
48
  */
21
49
  export class ReferenceLedger {
22
- order = [];
23
- markers = new Map();
50
+ references = [];
51
+ marks = new Map();
24
52
  /**
25
- * Give the marker number of a reference. A new reference gets the next number, and it joins the
26
- * order. A reference that matches an earlier one gives the earlier number, thus the list holds it one
27
- * time.
53
+ * Give the marker number of a reference. A new reference gets the next number of the ladder, and it
54
+ * joins the order of the ladder. A reference that matches an earlier one gives the earlier number, thus
55
+ * the appendix holds it one time.
56
+ *
57
+ * A citation matches on its key alone. The `raw` text and the display fields of a citation are the
58
+ * words of the author, and two blocks over one paper carry different words. The key names the paper,
59
+ * thus one paper takes one number and the appendix holds it one time.
28
60
  */
29
61
  mark(reference) {
30
- const key = serializeReference(reference);
31
- const seen = this.markers.get(key);
62
+ const key = reference.kind === "citation" ? citationKeyOf(reference) : serializeReference(reference);
63
+ const seen = this.marks.get(key);
32
64
  if (seen !== undefined) {
33
65
  return seen;
34
66
  }
35
- const marker = this.order.length + 1;
36
- this.markers.set(key, marker);
37
- this.order.push(reference);
38
- return marker;
67
+ // `push` gives back the new length of the array, which is the number of the entry that it added.
68
+ const n = this.references.push(reference);
69
+ this.marks.set(key, n);
70
+ return n;
39
71
  }
40
- /** The references in first-appearance order. The marker of an entry is its position plus one. */
72
+ /** Every reference in first-appearance order. The marker of an entry is its position plus one. */
41
73
  entries() {
42
- return this.order;
74
+ return this.references;
43
75
  }
44
76
  }
@@ -8,18 +8,36 @@
8
8
  *
9
9
  * One shared `ReferenceLedger` threads through the whole walk. Thus a claim marker and a citation marker
10
10
  * count by first appearance across the page, and a reference that two blocks share keeps one number and one
11
- * list entry.
11
+ * list entry. The ledger holds one ladder over both reference kinds, thus the page carries one marker
12
+ * notation and one appendix.
12
13
  *
13
14
  * The output is a pure function of the two inputs. The renderer reads no clock, no random value, and no
14
15
  * locale. Thus the same document and the same values give the same bytes.
15
16
  */
16
17
  import { type Result } from "neverthrow";
17
18
  import type { ReportDocument } from "../contracts/report-blocks.js";
18
- import type { RenderProblem, RenderValues } from "./types.js";
19
+ import { type CitationRecords } from "../report-model/reference-resolver.js";
20
+ import { type DerivationChain } from "./references.js";
21
+ import type { RenderedPage, RenderProblem, RenderValues } from "./types.js";
19
22
  /**
20
- * Render a report document and its value map to one HTML string.
23
+ * Render a report document, its value map, and the pinned citation records to one page and its data assets.
21
24
  *
22
25
  * The walk collects every value problem. When any problem exists, the render returns the problems and no
23
- * HTML. When no problem exists, the render assembles the page and returns the string.
26
+ * page. When no problem exists, the render assembles the page and returns it beside the assets that the
27
+ * page references.
28
+ *
29
+ * The records are the bibliography of the pin, and they are optional. A caller that passes none renders
30
+ * each citation from its key alone, thus a stored pin that holds no record map renders as it did before.
31
+ *
32
+ * The derivations are the chains of the session, and they are optional too. An appendix entry whose path a
33
+ * chain names states its sources and its script. A document that binds no derived path renders
34
+ * byte-identically with the chains and without them. A chain carries the relative source of its staged
35
+ * script and of its derived file as data, thus the entry links both and this module stages nothing.
36
+ *
37
+ * The renderer writes no file. The data assets ride the result, and the caller stages them beside the page.
38
+ * Thus the render stays pure, and two renders of one document give byte-identical assets.
39
+ *
40
+ * One artifact gives one payload. A table block and a chart block that bind it read that one asset, and a
41
+ * chart under the inline bound carries its rows in its own option and registers none.
24
42
  */
25
- export declare function renderReportPage(document: ReportDocument, values: RenderValues): Result<string, RenderProblem[]>;
43
+ export declare function renderReportPage(document: ReportDocument, values: RenderValues, records?: CitationRecords, derivations?: readonly DerivationChain[]): Result<RenderedPage, RenderProblem[]>;
@@ -8,51 +8,87 @@
8
8
  *
9
9
  * One shared `ReferenceLedger` threads through the whole walk. Thus a claim marker and a citation marker
10
10
  * count by first appearance across the page, and a reference that two blocks share keeps one number and one
11
- * list entry.
11
+ * list entry. The ledger holds one ladder over both reference kinds, thus the page carries one marker
12
+ * notation and one appendix.
12
13
  *
13
14
  * The output is a pure function of the two inputs. The renderer reads no clock, no random value, and no
14
15
  * locale. Thus the same document and the same values give the same bytes.
15
16
  */
16
17
  import { err, ok } from "neverthrow";
18
+ import { serializeReference } from "../contracts/report-reference.js";
19
+ import { citationRecordOf } from "../report-model/reference-resolver.js";
17
20
  import { renderChart } from "./views/chart-view.js";
18
- import { deriveChartOption } from "./chart.js";
21
+ import { deriveChartRender } from "./chart.js";
19
22
  import { assemblePage, renderBand, renderReferenceSection } from "./views/page-view.js";
20
23
  import { renderClaim, renderNav, renderSection, renderText } from "./views/prose.js";
21
- import { ReferenceLedger } from "./references.js";
22
- import { renderCitation, renderFigure, renderMetric, renderMetricGrid, renderTable } from "./views/values.js";
24
+ import { citationKeyOf, derivationChains, ReferenceLedger } from "./references.js";
25
+ import { encodeTablePayload, tableDataAsset } from "./table-data.js";
26
+ import { renderCitation, renderFigure, renderMetric, renderMetricGrid, renderTable, tableColumns, tableDisplay } from "./views/values.js";
23
27
  /**
24
- * Render a report document and its value map to one HTML string.
28
+ * Render a report document, its value map, and the pinned citation records to one page and its data assets.
25
29
  *
26
30
  * The walk collects every value problem. When any problem exists, the render returns the problems and no
27
- * HTML. When no problem exists, the render assembles the page and returns the string.
31
+ * page. When no problem exists, the render assembles the page and returns it beside the assets that the
32
+ * page references.
33
+ *
34
+ * The records are the bibliography of the pin, and they are optional. A caller that passes none renders
35
+ * each citation from its key alone, thus a stored pin that holds no record map renders as it did before.
36
+ *
37
+ * The derivations are the chains of the session, and they are optional too. An appendix entry whose path a
38
+ * chain names states its sources and its script. A document that binds no derived path renders
39
+ * byte-identically with the chains and without them. A chain carries the relative source of its staged
40
+ * script and of its derived file as data, thus the entry links both and this module stages nothing.
41
+ *
42
+ * The renderer writes no file. The data assets ride the result, and the caller stages them beside the page.
43
+ * Thus the render stays pure, and two renders of one document give byte-identical assets.
44
+ *
45
+ * One artifact gives one payload. A table block and a chart block that bind it read that one asset, and a
46
+ * chart under the inline bound carries its rows in its own option and registers none.
28
47
  */
29
- export function renderReportPage(document, values) {
48
+ export function renderReportPage(document, values, records, derivations) {
30
49
  const problems = [];
31
50
  const ledger = new ReferenceLedger();
51
+ const data = { payloads: new Map(), mounts: 0 };
32
52
  const content = [];
33
53
  for (const [index, section] of document.sections.entries()) {
34
- content.push(renderBand(index, renderBlock(section, values, ledger, 0, problems)));
54
+ content.push(renderBand(index, renderBlock(section, values, ledger, records, data, 0, problems)));
35
55
  }
36
56
  if (problems.length > 0) {
37
57
  return err(problems);
38
58
  }
59
+ const dataAssets = [...data.payloads.values()].map((entry) => tableDataAsset(entry.ids, entry.payload));
39
60
  const nav = renderNav(document.sections);
40
- const references = renderReferenceSection(ledger, document.sections.length);
41
- return ok(assemblePage(document.title, nav, content.join(""), references));
61
+ const references = renderReferenceSection(ledger, document.sections.length, records, derivationChains(derivations));
62
+ return ok({ html: assemblePage(document.title, nav, content.join(""), references, dataAssets, data.mounts > 0), dataAssets });
63
+ }
64
+ /**
65
+ * Register the payload of one bound artifact, and name this block as a reader of it.
66
+ *
67
+ * The first block of an artifact encodes the payload. A later block over the same binding adds its id and
68
+ * encodes nothing, thus the page carries one copy of the rows.
69
+ */
70
+ function registerPayload(data, binding, blockId, encode) {
71
+ const key = serializeReference(binding);
72
+ const registered = data.payloads.get(key);
73
+ if (registered !== undefined) {
74
+ registered.ids.push(blockId);
75
+ return;
76
+ }
77
+ data.payloads.set(key, { ids: [blockId], payload: encode() });
42
78
  }
43
79
  /**
44
80
  * Render one block to its HTML, and collect a value problem into `problems`. A block whose value is missing
45
81
  * or of the wrong shape adds one problem and renders nothing. The empty result drops out, because the whole
46
82
  * render fails when any problem exists.
47
83
  */
48
- function renderBlock(block, values, ledger, depth, problems) {
84
+ function renderBlock(block, values, ledger, records, data, depth, problems) {
49
85
  switch (block.kind) {
50
86
  case "text":
51
87
  return renderText(block);
52
88
  case "claim":
53
89
  return renderClaim(block, ledger);
54
90
  case "citation":
55
- return renderCitation(block, ledger);
91
+ return renderCitation(block, ledger, citationRecordOf(records, citationKeyOf(block.binding)));
56
92
  case "metric": {
57
93
  const entry = values[block.id];
58
94
  if (entry === undefined) {
@@ -63,7 +99,7 @@ function renderBlock(block, values, ledger, depth, problems) {
63
99
  problems.push(wrongShape(block.id, entry.type, "scalar"));
64
100
  return "";
65
101
  }
66
- return renderMetric(block, entry);
102
+ return renderMetric(block, ledger, entry);
67
103
  }
68
104
  case "table": {
69
105
  const entry = values[block.id];
@@ -75,7 +111,13 @@ function renderBlock(block, values, ledger, depth, problems) {
75
111
  problems.push(wrongShape(block.id, entry.type, "table"));
76
112
  return "";
77
113
  }
78
- return renderTable(block, entry);
114
+ // The rows of the block ride a data asset, and the card holds one empty mount. The payload
115
+ // carries the column order, thus a cell of an encoded row names its column by position and the
116
+ // display entry of that column sits at the same index.
117
+ const columns = tableColumns(entry);
118
+ data.mounts += 1;
119
+ registerPayload(data, block.binding, block.id, () => payloadOf(block.binding, entry, columns));
120
+ return renderTable(block, ledger, entry.rows.length, entry.total);
79
121
  }
80
122
  case "figure": {
81
123
  const entry = values[block.id];
@@ -87,7 +129,7 @@ function renderBlock(block, values, ledger, depth, problems) {
87
129
  problems.push(wrongShape(block.id, entry.type, "figure"));
88
130
  return "";
89
131
  }
90
- return renderFigure(block, entry);
132
+ return renderFigure(block, ledger, entry);
91
133
  }
92
134
  case "chart": {
93
135
  const entry = values[block.id];
@@ -99,31 +141,48 @@ function renderBlock(block, values, ledger, depth, problems) {
99
141
  problems.push(wrongShape(block.id, entry.type, "table"));
100
142
  return "";
101
143
  }
102
- const option = deriveChartOption(block, entry.rows, entry.columns);
103
- if (option.isErr()) {
104
- problems.push(option.error);
144
+ // A chart whose inline option would carry every row reads the payload of its artifact instead.
145
+ // The payload key is the block id, thus a chart and a table over one artifact read one payload
146
+ // under two ids. One binding resolves one row set, thus the column places and the row places of
147
+ // the descriptors address the registered payload whichever block encoded it.
148
+ const columns = tableColumns(entry);
149
+ const derived = deriveChartRender(block, entry.rows, entry.columns, { key: block.id, columns });
150
+ if (derived.isErr()) {
151
+ problems.push(derived.error);
105
152
  return "";
106
153
  }
107
- return renderChart(block, option.value);
154
+ if (derived.value.readsPayload) {
155
+ registerPayload(data, block.binding, block.id, () => payloadOf(block.binding, entry, columns));
156
+ }
157
+ return renderChart(block, ledger, derived.value.option);
108
158
  }
109
159
  case "section":
110
- return renderSection(block, depth, renderChildren(block.blocks, values, ledger, depth + 1, problems));
160
+ return renderSection(block, depth, renderChildren(block.blocks, values, ledger, records, data, depth + 1, problems));
111
161
  }
112
162
  }
163
+ /**
164
+ * The payload of one bound artifact: the encoded rows, the display of each column, and the pre-bound total.
165
+ *
166
+ * A table and a chart bind the same whole-table reference, thus one function serves both and the two read
167
+ * one shape. The binding gives the declared labels and meanings, and the value gives the rows.
168
+ */
169
+ function payloadOf(binding, entry, columns) {
170
+ return encodeTablePayload(columns, entry.rows, tableDisplay(binding, entry, columns), entry.total);
171
+ }
113
172
  /**
114
173
  * Render the children of one section, and group a consecutive run of metric siblings into one grid.
115
174
  *
116
175
  * A run of two or more metric blocks reads as one row of statistics, thus the grid holds the whole run. A
117
176
  * lone metric stays one card, and no grid wraps it. A block of a different kind ends the run.
118
177
  */
119
- function renderChildren(blocks, values, ledger, depth, problems) {
178
+ function renderChildren(blocks, values, ledger, records, data, depth, problems) {
120
179
  const parts = [];
121
180
  let index = 0;
122
181
  while (index < blocks.length) {
123
182
  const run = metricRunLength(blocks, index);
124
183
  const rendered = [];
125
184
  for (let offset = 0; offset < Math.max(run, 1); offset += 1) {
126
- rendered.push(renderBlock(blocks[index + offset], values, ledger, depth, problems));
185
+ rendered.push(renderBlock(blocks[index + offset], values, ledger, records, data, depth, problems));
127
186
  }
128
187
  parts.push(run > 1 ? renderMetricGrid(rendered.join("")) : rendered.join(""));
129
188
  index += Math.max(run, 1);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The data-script payload of one table block.
3
+ *
4
+ * A table of many thousands of rows stamped into the markup makes a page of many megabytes. A `fetch` is
5
+ * refused on a `file://` page, thus a data file cannot load the way an image does. A classic script asset
6
+ * loads on any page, and the payload registers itself under one global map when the browser runs it.
7
+ *
8
+ * The encode is columnar. The rows are arrays in column order, thus no row repeats the column names. A
9
+ * string that occurs more than one time in a column moves into the dictionary of that column, and each
10
+ * cell that holds it becomes its index. A category column of one thousand rows and four values then costs
11
+ * four strings and one thousand small integers.
12
+ *
13
+ * The payload also carries the display of each column: the header label, the number kind, and the bound of
14
+ * a stored zero. The server resolves the three, and the page formats each cell under them. It carries the
15
+ * pre-bound row total of the artifact beside them, thus the footer states the shown count against it.
16
+ *
17
+ * The module is pure, and it reads no file. The renderer derives a payload, and the caller writes it.
18
+ */
19
+ import type { NumberKind } from "./number-format.js";
20
+ /**
21
+ * The global map that each payload registers under, keyed by the block id.
22
+ *
23
+ * The page script reads the same name, thus the payload and the reader cannot disagree over a rename.
24
+ */
25
+ export declare const TABLE_DATA_GLOBAL = "__REPORT_TABLES";
26
+ /** One cell of an encoded row: a raw value, a dictionary index, or `null` for a cell that the row lacks. */
27
+ export type EncodedCell = string | number | null;
28
+ /** The filter that a column takes on the page: the number filter of a magnitude, or the text filter of a name. */
29
+ export type ColumnFilter = "number" | "text";
30
+ /**
31
+ * How one column shows on the page: the header label, the number kind of the column, the filter of the
32
+ * column, and the bound of a stored zero where the column holds one.
33
+ *
34
+ * The server resolves each of the four, and the page reads them. Thus the declaration, the token guess, and
35
+ * the read of a whole column stay on the server, and the page formats and filters alone.
36
+ */
37
+ export interface ColumnDisplay {
38
+ label: string;
39
+ kind: NumberKind;
40
+ filter: ColumnFilter;
41
+ bound?: number;
42
+ }
43
+ /**
44
+ * One encoded table.
45
+ *
46
+ * `columns` is the column order, and each row of `rows` holds one cell for each column in that order.
47
+ * `dict` holds the repeated values of a column, keyed by the column name. A cell of such a column is a
48
+ * number when it names an entry of that list, and the value itself at every other time.
49
+ *
50
+ * `display` holds one entry for each column, in the same order. A list and not a map, thus a column named
51
+ * `__proto__` needs no guard on the page and the entry of a column sits at the index of its name.
52
+ *
53
+ * `total` is the row count of the artifact before a row bound cut it. The page states the shown count
54
+ * against it, thus a reader of a cut table never reads the cut as the whole artifact.
55
+ */
56
+ export interface TablePayload {
57
+ columns: string[];
58
+ rows: EncodedCell[][];
59
+ dict: Record<string, string[]>;
60
+ display: ColumnDisplay[];
61
+ total: number;
62
+ }
63
+ /** One data asset that the caller writes beside the page: the staged file name, and the source text of it. */
64
+ export interface DataAsset {
65
+ name: string;
66
+ bytes: string;
67
+ }
68
+ /**
69
+ * Encode a resolved table into its payload.
70
+ *
71
+ * The caller gives the column order, and the header of the card reads that same order. Thus the position
72
+ * of a cell in a row names its column.
73
+ *
74
+ * A cell that a row does not hold rides as `null`. A table tolerates a ragged row, thus the payload states
75
+ * the absence in place and never shifts the rest of the row.
76
+ *
77
+ * `display` arrives in the column order, thus the entry of a column sits at the index of its name.
78
+ *
79
+ * `total` is the row count of the artifact before a row bound cut it. A caller that resolved no bound knows
80
+ * no larger count, thus it gives none and the row count answers.
81
+ */
82
+ export declare function encodeTablePayload(columns: readonly string[], rows: ReadonlyArray<Record<string, string | number>>, display: readonly ColumnDisplay[], total?: number): TablePayload;
83
+ /**
84
+ * The data asset of one bound artifact: the content-addressed file name, and the source text.
85
+ *
86
+ * The name carries the hash of the bytes, in the content-address style of a staged figure. Thus two
87
+ * previews of unchanged data give one name, a changed table gives a new name, and the sweep of the stage
88
+ * removes the name that nothing references any more.
89
+ *
90
+ * The block ids are the readers of the payload, in document order.
91
+ */
92
+ export declare function tableDataAsset(blockIds: readonly string[], payload: TablePayload): DataAsset;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The data-script payload of one table block.
3
+ *
4
+ * A table of many thousands of rows stamped into the markup makes a page of many megabytes. A `fetch` is
5
+ * refused on a `file://` page, thus a data file cannot load the way an image does. A classic script asset
6
+ * loads on any page, and the payload registers itself under one global map when the browser runs it.
7
+ *
8
+ * The encode is columnar. The rows are arrays in column order, thus no row repeats the column names. A
9
+ * string that occurs more than one time in a column moves into the dictionary of that column, and each
10
+ * cell that holds it becomes its index. A category column of one thousand rows and four values then costs
11
+ * four strings and one thousand small integers.
12
+ *
13
+ * The payload also carries the display of each column: the header label, the number kind, and the bound of
14
+ * a stored zero. The server resolves the three, and the page formats each cell under them. It carries the
15
+ * pre-bound row total of the artifact beside them, thus the footer states the shown count against it.
16
+ *
17
+ * The module is pure, and it reads no file. The renderer derives a payload, and the caller writes it.
18
+ */
19
+ import { createHash } from "node:crypto";
20
+ import { scriptJson } from "./script-json.js";
21
+ /**
22
+ * The global map that each payload registers under, keyed by the block id.
23
+ *
24
+ * The page script reads the same name, thus the payload and the reader cannot disagree over a rename.
25
+ */
26
+ export const TABLE_DATA_GLOBAL = "__REPORT_TABLES";
27
+ /** The count of hash characters in an asset name. It matches the sidecar of an artifact, thus one form reads across the directory. */
28
+ const HASH_CHARS = 12;
29
+ /**
30
+ * The dictionary of one column, or `undefined` when the column takes none.
31
+ *
32
+ * A column takes a dictionary when every cell of it is a string. The condition keeps the decode
33
+ * unambiguous: a number in such a column can only be an index, and a string can only be a value. A column
34
+ * that holds one number rides raw, thus no reader must tell an index from a measurement.
35
+ *
36
+ * The dictionary holds each string that occurs more than one time, in first-appearance order. A string
37
+ * that occurs one time saves nothing as an entry, thus it stays in its row. The order is a function of the
38
+ * rows alone, thus one table gives one dictionary and two encodes of it match.
39
+ */
40
+ function columnDictionary(rows, column) {
41
+ const counts = new Map();
42
+ for (const row of rows) {
43
+ const cell = row[column];
44
+ if (typeof cell !== "string") {
45
+ return undefined;
46
+ }
47
+ counts.set(cell, (counts.get(cell) ?? 0) + 1);
48
+ }
49
+ const repeated = [];
50
+ for (const [value, count] of counts) {
51
+ if (count > 1) {
52
+ repeated.push(value);
53
+ }
54
+ }
55
+ return repeated.length > 0 ? repeated : undefined;
56
+ }
57
+ /**
58
+ * Encode a resolved table into its payload.
59
+ *
60
+ * The caller gives the column order, and the header of the card reads that same order. Thus the position
61
+ * of a cell in a row names its column.
62
+ *
63
+ * A cell that a row does not hold rides as `null`. A table tolerates a ragged row, thus the payload states
64
+ * the absence in place and never shifts the rest of the row.
65
+ *
66
+ * `display` arrives in the column order, thus the entry of a column sits at the index of its name.
67
+ *
68
+ * `total` is the row count of the artifact before a row bound cut it. A caller that resolved no bound knows
69
+ * no larger count, thus it gives none and the row count answers.
70
+ */
71
+ export function encodeTablePayload(columns, rows, display, total) {
72
+ const dict = {};
73
+ const indexes = new Map();
74
+ for (const column of columns) {
75
+ const values = columnDictionary(rows, column);
76
+ if (values === undefined) {
77
+ continue;
78
+ }
79
+ dict[column] = values;
80
+ indexes.set(column, new Map(values.map((value, index) => [value, index])));
81
+ }
82
+ const encodedRows = rows.map((row) => columns.map((column) => {
83
+ const cell = row[column];
84
+ if (cell === undefined) {
85
+ return null;
86
+ }
87
+ // A dictionary column holds strings alone, thus a cell of another type never carries an index.
88
+ const index = typeof cell === "string" ? indexes.get(column)?.get(cell) : undefined;
89
+ return index === undefined ? cell : index;
90
+ }));
91
+ return { columns: [...columns], rows: encodedRows, dict, display: [...display], total: total ?? rows.length };
92
+ }
93
+ /**
94
+ * The source text of one payload asset.
95
+ *
96
+ * The text assigns into the global map under the block id. The map takes the null-prototype form, thus a
97
+ * block id such as `constructor` stays an ordinary entry. The block id rides as JSON, thus hostile text is
98
+ * data and never source.
99
+ *
100
+ * A table block and a chart block over one artifact read one payload. The first id takes the parsed value,
101
+ * and each further id takes the same object. Thus the page holds one copy of the rows, and each reader
102
+ * finds it under its own block id.
103
+ *
104
+ * The payload itself rides through `JSON.parse` and never as an object literal. An object literal with a
105
+ * `__proto__` key sets the prototype of the object instead of an own entry, thus a column of that name
106
+ * would arrive as a mangled object with no such column. `JSON.parse` defines an own property for every
107
+ * key, thus each column name reaches the page as a column name.
108
+ */
109
+ function payloadSource(blockIds, payload) {
110
+ const registry = `window.${TABLE_DATA_GLOBAL}`;
111
+ const json = scriptJson(JSON.stringify(payload));
112
+ const primary = `${registry}[${scriptJson(blockIds[0])}]`;
113
+ const aliases = blockIds.slice(1).map((id) => `${registry}[${scriptJson(id)}]=${primary};`);
114
+ return `${registry}=${registry}||Object.create(null);${primary}=JSON.parse(${json});${aliases.join("")}\n`;
115
+ }
116
+ /**
117
+ * The data asset of one bound artifact: the content-addressed file name, and the source text.
118
+ *
119
+ * The name carries the hash of the bytes, in the content-address style of a staged figure. Thus two
120
+ * previews of unchanged data give one name, a changed table gives a new name, and the sweep of the stage
121
+ * removes the name that nothing references any more.
122
+ *
123
+ * The block ids are the readers of the payload, in document order.
124
+ */
125
+ export function tableDataAsset(blockIds, payload) {
126
+ const bytes = payloadSource(blockIds, payload);
127
+ const hash = createHash("sha256").update(bytes).digest("hex").slice(0, HASH_CHARS);
128
+ return { name: `t-${hash}.data.js`, bytes };
129
+ }