@north-light/crouter 0.3.196 → 0.3.198

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 (148) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +90 -168
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/termrender/display.d.ts +7 -3
  91. package/dist/core/termrender/display.js +45 -9
  92. package/dist/core/termrender/termrender.d.ts +3 -12
  93. package/dist/core/termrender/termrender.js +6 -37
  94. package/dist/core/user-settings.d.ts +17 -2
  95. package/dist/core/user-settings.js +32 -11
  96. package/dist/daemon/api/handlers/human.js +1 -1
  97. package/dist/daemon/api/handlers/inbox.js +22 -7
  98. package/dist/daemon/cron-run.js +16 -2
  99. package/dist/daemon/crtrd.js +1 -1
  100. package/dist/daemon/fleet.d.ts +22 -10
  101. package/dist/daemon/fleet.js +43 -24
  102. package/dist/daemon/human/finish.d.ts +2 -1
  103. package/dist/daemon/human/finish.js +45 -28
  104. package/dist/daemon/human/sweep.js +4 -2
  105. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  106. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  107. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  108. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  109. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  110. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  111. package/dist/daemon/review/sweep.js +12 -1
  112. package/dist/pages/bundle.css +1 -1
  113. package/dist/pages/bundle.js +1008 -577
  114. package/dist/pages/comments.d.ts +29 -0
  115. package/dist/pages/comments.js +51 -0
  116. package/dist/pages/controls.d.ts +98 -0
  117. package/dist/pages/controls.js +261 -0
  118. package/dist/pages/elements/cards.d.ts +15 -4
  119. package/dist/pages/elements/cards.js +299 -186
  120. package/dist/pages/elements/chart.d.ts +22 -0
  121. package/dist/pages/elements/chart.js +107 -91
  122. package/dist/pages/elements/options.d.ts +9 -0
  123. package/dist/pages/elements/options.js +268 -155
  124. package/dist/pages/elements/pages.d.ts +6 -0
  125. package/dist/pages/elements/pages.js +60 -38
  126. package/dist/pages/elements/slot.d.ts +5 -0
  127. package/dist/pages/elements/slot.js +17 -10
  128. package/dist/pages/elements/table.d.ts +17 -0
  129. package/dist/pages/elements/table.js +336 -228
  130. package/dist/pages/elements/text.d.ts +9 -3
  131. package/dist/pages/elements/text.js +350 -143
  132. package/dist/pages/host.d.ts +0 -24
  133. package/dist/pages/host.js +0 -17
  134. package/dist/pages/readonly.d.ts +24 -0
  135. package/dist/pages/readonly.js +31 -0
  136. package/dist/pages/responses.d.ts +37 -0
  137. package/dist/pages/responses.js +56 -0
  138. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  139. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  140. package/dist/types.d.ts +19 -5
  141. package/dist/types.js +2 -1
  142. package/package.json +1 -1
  143. package/runtime.lock.json +2 -2
  144. package/scripts/install-runtime.mjs +19 -24
  145. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  146. package/dist/clients/attach/render/html-markdown.js +0 -206
  147. package/dist/core/human/markdown-html.d.ts +0 -8
  148. package/dist/core/human/markdown-html.js +0 -581
@@ -0,0 +1,161 @@
1
+ const ELEMENT_RULES = `Element contract
2
+ - Put the JSON config in the first direct \`<script type="application/json">\` child of the component element. It must contain one JSON object.
3
+ - \`id\` must match \`^[A-Za-z0-9_-]{1,64}$\`, be unique across the page when present, and is required only for response-bearing components. Display-only components do not need an id.
4
+ - When a page has two or more response-bearing components, every one needs a nonempty \`config.label\`.
5
+ - Config objects and every documented nested object reject unknown fields.`;
6
+ const COMMENT_SHAPE = `Comments are \`{id:string (nonempty), text:string, anchor:Anchor}\`. Every response carries \`comments\` as an array. \`Anchor\` is one of \`{kind:"whole"}\`, \`{kind:"option",optionId:string}\`, \`{kind:"row",rowId:string}\`, \`{kind:"column",columnId:string}\`, \`{kind:"card",cardId:string}\`, or \`{kind:"range",start:nonnegative integer,end:nonnegative integer >= start,quote:string}\`; each component accepts only the anchors named below.`;
7
+ export const BUILTIN_COMPONENT_DOCS = [
8
+ {
9
+ kind: 'crtr-pages',
10
+ tag: '<crtr-pages>',
11
+ selectionLine: '`<crtr-pages>` — step wrapper. Use when a page is interactive or multi-step; it is always the wrapper for interactive/multi-step pages.',
12
+ contract: `\`<crtr-pages>\`
13
+
14
+ Display-only structural primitive. It takes no config or response.
15
+
16
+ - Use exactly one wrapper when the page has a response-bearing component or more than one step; do not nest wrappers.
17
+ - It contains one or more \`<crtr-page>\` elements. A wrapper with no steps is invalid.`,
18
+ },
19
+ {
20
+ kind: 'crtr-page',
21
+ tag: '<crtr-page>',
22
+ selectionLine: '`<crtr-page>` — one page step. Use inside `<crtr-pages>` to group a step of an interactive or multi-step page.',
23
+ contract: `\`<crtr-page>\`
24
+
25
+ Display-only structural primitive. It takes no config or response.
26
+
27
+ - Every \`<crtr-page>\` is a direct-or-descendant step of the one \`<crtr-pages>\` wrapper; it may not appear outside that wrapper.
28
+ - Response-bearing components must be inside a \`<crtr-page>\`. Step order is document order.`,
29
+ },
30
+ {
31
+ kind: 'options',
32
+ tag: '<crtr-options>',
33
+ selectionLine: '`<crtr-options>` — known-alternative picker. Use when the human picks among known alternatives.',
34
+ contract: `\`<crtr-options>\`
35
+
36
+ Response-bearing. The response is \`{selectedOptionIds:string[], comments:Comment[], freetext?:string}\`.
37
+
38
+ ${ELEMENT_RULES}
39
+
40
+ Config JSON
41
+ - \`options\` — required nonempty array of \`{id:string (nonempty, unique), label:string (nonempty), description?:string}\`.
42
+ - \`mode\` — required: \`single\` or \`multi\`.
43
+ - \`label?:string\` — slot label; required by the multi-response page rule.
44
+ - \`allowFreetext?:boolean\`, \`freetextLabel?:string\`, \`freetextPlaceholder?:string\` — optional free-text affordance fields.
45
+
46
+ Response rules
47
+ - \`selectedOptionIds\` contains unique known option ids. A \`single\` response has at most one selected id and requires exactly one selection or nonempty allowed \`freetext\`; a \`multi\` response requires a selection, comment, or nonempty allowed \`freetext\`.
48
+ - \`freetext\` is accepted only when \`allowFreetext:true\`.
49
+ - ${COMMENT_SHAPE} Options accept only \`whole\` and \`option\` anchors; \`optionId\` must name a configured option.`,
50
+ },
51
+ {
52
+ kind: 'text',
53
+ tag: '<crtr-text>',
54
+ selectionLine: '`<crtr-text>` — prose field. Use when presenting or collecting prose; use `singleLine` for a subject, name, or URL.',
55
+ contract: `\`<crtr-text>\`
56
+
57
+ Response-bearing. The response is \`{text:string, edited:boolean, comments:Comment[]}\`.
58
+
59
+ ${ELEMENT_RULES}
60
+
61
+ Config JSON
62
+ - \`initialText\` — required string, including an empty string.
63
+ - \`label?:string\` — slot label; required by the multi-response page rule.
64
+ - \`editable?:boolean\`, \`placeholder?:string\`, \`singleLine?:boolean\` — optional presentation/input settings.
65
+ - \`singleLine:true\` is mutually exclusive with \`editable:false\`: a single-line field is always editable.
66
+
67
+ Response rules
68
+ - ${COMMENT_SHAPE} Text accepts only \`whole\` and \`range\` anchors.`,
69
+ },
70
+ {
71
+ kind: 'table',
72
+ tag: '<crtr-table>',
73
+ selectionLine: '`<crtr-table>` — structured rows and columns. Use when the human needs to inspect or select tabular data.',
74
+ contract: `\`<crtr-table>\`
75
+
76
+ Display-only when both selection modes are \`none\`; response-bearing when \`rowSelect\` or \`columnSelect\` is \`single\` or \`multi\`. Its response when selectable is \`{selectedRowIds:string[], selectedColumnIds:string[], comments:Comment[]}\`.
77
+
78
+ ${ELEMENT_RULES}
79
+
80
+ Config JSON
81
+ - \`columns\` — required nonempty array of \`{id:string (nonempty, unique), label:string (nonempty), align?:"left"|"right", mono?:boolean}\`.
82
+ - \`rows?:\` array of \`{id:string (nonempty, unique), cells:Record<string,string|number|boolean|null>}\`.
83
+ - \`source?:string\` — path to a local JSON file containing the rows array. At submit it is read, validated as \`rows\`, and embedded; the stored ticket is self-contained and elements never see \`source\`. \`rows\` and \`source\` are mutually exclusive.
84
+ - \`rowSelect?:"none"|"single"|"multi"\` — defaults to \`none\`.
85
+ - \`columnSelect?:"none"|"single"|"multi"\` — defaults to \`none\`.
86
+ - \`label?:string\` — slot label; required by the multi-response page rule when the table is selectable.
87
+
88
+ Response rules when selectable
89
+ - Selected row and column ids are unique and must name configured ids. \`none\` permits no corresponding selected ids; \`single\` permits at most one.
90
+ - ${COMMENT_SHAPE} Tables accept only \`whole\`, \`row\`, and \`column\` anchors. Row and column anchors must name configured ids.`,
91
+ },
92
+ {
93
+ kind: 'cards',
94
+ tag: '<crtr-cards>',
95
+ selectionLine: '`<crtr-cards>` — visual item picker. Use when the human chooses one or more rich, card-shaped alternatives.',
96
+ contract: `\`<crtr-cards>\`
97
+
98
+ Response-bearing. The response is \`{selectedCardIds:string[], comments:Comment[]}\`.
99
+
100
+ ${ELEMENT_RULES}
101
+
102
+ Config JSON
103
+ - \`cards?:\` array of \`{id:string (nonempty, unique), title:string (nonempty), subtitle?:string, body?:string, tag?:string}\`.
104
+ - \`source?:string\` — path to a local JSON file containing the cards array. At submit it is read, validated as \`cards\`, and embedded; the stored ticket is self-contained and elements never see \`source\`. \`cards\` and \`source\` are mutually exclusive.
105
+ - \`mode\` — required: \`single\` or \`multi\`.
106
+ - \`columns?:2|3\` — optional layout column count.
107
+ - \`label?:string\` — slot label; required by the multi-response page rule.
108
+
109
+ Response rules
110
+ - \`selectedCardIds\` contains unique configured card ids. \`single\` permits at most one selected card.
111
+ - ${COMMENT_SHAPE} Cards accept only \`whole\` and \`card\` anchors. Card anchors must name configured ids.`,
112
+ },
113
+ {
114
+ kind: 'chart',
115
+ tag: '<crtr-chart>',
116
+ selectionLine: '`<crtr-chart>` — data visualization. Use when a chart communicates a trend or comparison better than prose or a table.',
117
+ contract: `\`<crtr-chart>\`
118
+
119
+ Display-only; it has no response.
120
+
121
+ ${ELEMENT_RULES}
122
+
123
+ Config JSON
124
+ - \`kind\` — required: \`line\`, \`bar\`, or \`area\`.
125
+ - \`x\` — required nonempty string naming the x field.
126
+ - \`y\` — required nonempty array of \`{field:string (nonempty), label?:string}\` series.
127
+ - \`title?:string\`, \`data?:\` array of JSON row objects — optional display inputs.
128
+ - \`source?:string\` — path to a local JSON file containing the \`data\` array. At submit it is read, validated as \`data\`, and embedded; the stored ticket is self-contained and elements never see \`source\`. \`data\` and \`source\` are mutually exclusive.`,
129
+ },
130
+ ];
131
+ const docsByKind = new Map(BUILTIN_COMPONENT_DOCS.map((doc) => [doc.kind, doc]));
132
+ /** Compact catalog rows shared by `human -h` and `human html list`. */
133
+ export const BUILTIN_COMPONENT_SELECTION_LINES = BUILTIN_COMPONENT_DOCS.map((doc) => doc.selectionLine);
134
+ export function componentDoc(kind) {
135
+ return docsByKind.get(kind);
136
+ }
137
+ export function componentKinds(productKinds) {
138
+ return [...BUILTIN_COMPONENT_DOCS.map((doc) => doc.kind), ...productKinds];
139
+ }
140
+ export function renderComponentCatalog(productComponents) {
141
+ const lines = [`<components count="${BUILTIN_COMPONENT_DOCS.length + productComponents.length}">`, ...BUILTIN_COMPONENT_SELECTION_LINES];
142
+ for (const component of productComponents) {
143
+ if (component.description === undefined && component.useWhen === undefined) {
144
+ lines.push('', `\`${component.kind}\``, component.display === true ? 'product-registered; display-only' : 'product-registered');
145
+ continue;
146
+ }
147
+ const details = [component.description, component.useWhen === undefined ? undefined : `${component.useWhen[0].toUpperCase()}${component.useWhen.slice(1)}`, ...(component.display === true ? ['Display-only'] : [])].filter((detail) => detail !== undefined);
148
+ lines.push('', `\`${component.kind}\` — ${details.join('. ')}`);
149
+ }
150
+ lines.push('</components>');
151
+ return lines.join('\n');
152
+ }
153
+ export function renderComponentContract(kind, productComponents) {
154
+ const builtin = componentDoc(kind);
155
+ if (builtin !== undefined)
156
+ return builtin.contract;
157
+ const productComponent = productComponents.find((component) => component.kind === kind);
158
+ if (productComponent !== undefined)
159
+ return `\`${kind}\`\n\n${productComponent.doc ?? `product-registered${productComponent.display === true ? '; display-only' : '; config is passed through unvalidated (unvalidated: true)'} — see the product's own docs`}`;
160
+ return undefined;
161
+ }
@@ -0,0 +1,3 @@
1
+ export declare const RICH_PAGE_PLACEHOLDER = "rich page \u2014 open in Northlight";
2
+ /** Downconvert the semantic prose in one HTML fragment or complete document. */
3
+ export declare function renderHtmlProseAsMarkdown(html: string): string;
@@ -0,0 +1,288 @@
1
+ // Bounded semantic HTML → Markdown for terminal page prose. This is deliberately
2
+ // not an HTML renderer: slots keep their own terminal panels, while custom chrome
3
+ // is reduced to text or one clear Northlight handoff.
4
+ export const RICH_PAGE_PLACEHOLDER = 'rich page — open in Northlight';
5
+ const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
6
+ const RAW_TEXT_TAGS = new Set(['script', 'style', 'template']);
7
+ const RICH_TAGS = new Set(['audio', 'button', 'canvas', 'dialog', 'embed', 'form', 'iframe', 'img', 'input', 'math', 'object', 'picture', 'select', 'svg', 'textarea', 'video']);
8
+ const BLOCK_TAGS = new Set(['article', 'aside', 'blockquote', 'body', 'div', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'li', 'main', 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul']);
9
+ const INLINE_BREAK = '\uE001';
10
+ /** Downconvert the semantic prose in one HTML fragment or complete document. */
11
+ export function renderHtmlProseAsMarkdown(html) {
12
+ const root = { type: 'element', tag: '#root', attrs: new Map(), children: parseHtml(html) };
13
+ const body = findElement(root, 'body');
14
+ return renderBlocks(body?.children ?? root.children).trim();
15
+ }
16
+ function parseHtml(html) {
17
+ const root = { type: 'element', tag: '#root', attrs: new Map(), children: [] };
18
+ const stack = [root];
19
+ let cursor = 0;
20
+ const appendText = (value) => { if (value !== '')
21
+ stack.at(-1).children.push({ type: 'text', value }); };
22
+ while (cursor < html.length) {
23
+ const start = html.indexOf('<', cursor);
24
+ if (start === -1) {
25
+ appendText(html.slice(cursor));
26
+ break;
27
+ }
28
+ appendText(html.slice(cursor, start));
29
+ if (html.startsWith('<!--', start)) {
30
+ const end = html.indexOf('-->', start + 4);
31
+ cursor = end === -1 ? html.length : end + 3;
32
+ continue;
33
+ }
34
+ const end = tagEnd(html, start);
35
+ if (end === -1) {
36
+ appendText(html.slice(start));
37
+ break;
38
+ }
39
+ const token = html.slice(start, end);
40
+ if (/^<!|^<\?/i.test(token)) {
41
+ cursor = end;
42
+ continue;
43
+ }
44
+ const closing = /^<\s*\/\s*([^\s>]+)/.exec(token);
45
+ if (closing !== null) {
46
+ const tag = closing[1].toLowerCase();
47
+ for (let index = stack.length - 1; index > 0; index--) {
48
+ if (stack[index].tag === tag) {
49
+ stack.length = index;
50
+ break;
51
+ }
52
+ }
53
+ cursor = end;
54
+ continue;
55
+ }
56
+ const opening = /^<\s*([^\s/>]+)/.exec(token);
57
+ if (opening === null) {
58
+ appendText(token);
59
+ cursor = end;
60
+ continue;
61
+ }
62
+ const tag = opening[1].toLowerCase();
63
+ const element = { type: 'element', tag, attrs: parseAttributes(token), children: [] };
64
+ stack.at(-1).children.push(element);
65
+ const selfClosing = /\/\s*>$/.test(token) || VOID_TAGS.has(tag);
66
+ if (RAW_TEXT_TAGS.has(tag) && !selfClosing) {
67
+ const close = new RegExp(`<\\s*/\\s*${escapeRegExp(tag)}\\s*>`, 'ig');
68
+ close.lastIndex = end;
69
+ const match = close.exec(html);
70
+ if (match !== null) {
71
+ element.children.push({ type: 'text', value: html.slice(end, match.index) });
72
+ cursor = close.lastIndex;
73
+ }
74
+ else
75
+ cursor = html.length;
76
+ continue;
77
+ }
78
+ if (!selfClosing)
79
+ stack.push(element);
80
+ cursor = end;
81
+ }
82
+ return root.children;
83
+ }
84
+ function tagEnd(html, start) {
85
+ let quote;
86
+ for (let index = start + 1; index < html.length; index++) {
87
+ const character = html[index];
88
+ if (quote !== undefined) {
89
+ if (character === quote)
90
+ quote = undefined;
91
+ continue;
92
+ }
93
+ if (character === '"' || character === "'") {
94
+ quote = character;
95
+ continue;
96
+ }
97
+ if (character === '>')
98
+ return index + 1;
99
+ }
100
+ return -1;
101
+ }
102
+ function parseAttributes(token) {
103
+ const attrs = new Map();
104
+ const source = token.slice(1, -1);
105
+ const firstSpace = source.search(/\s/);
106
+ if (firstSpace === -1)
107
+ return attrs;
108
+ const body = source.slice(firstSpace);
109
+ const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
110
+ for (const match of body.matchAll(pattern))
111
+ attrs.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '');
112
+ return attrs;
113
+ }
114
+ function findElement(node, tag) {
115
+ if (node.tag === tag)
116
+ return node;
117
+ for (const child of node.children)
118
+ if (child.type === 'element') {
119
+ const found = findElement(child, tag);
120
+ if (found !== undefined)
121
+ return found;
122
+ }
123
+ return undefined;
124
+ }
125
+ function renderBlocks(nodes) {
126
+ const blocks = [];
127
+ for (const node of nodes) {
128
+ const block = renderBlock(node).trim();
129
+ if (block !== '')
130
+ blocks.push(block);
131
+ }
132
+ return blocks.join('\n\n');
133
+ }
134
+ function renderBlock(node) {
135
+ if (node.type === 'text')
136
+ return markdownText(node.value).trim();
137
+ if (isInteractiveSlot(node))
138
+ return '';
139
+ if (isStructuralWrapper(node))
140
+ return renderBlocks(node.children);
141
+ if (isRichBlock(node))
142
+ return RICH_PAGE_PLACEHOLDER;
143
+ if (/^h[1-6]$/.test(node.tag))
144
+ return `${'#'.repeat(Number(node.tag[1]))} ${renderInline(node.children)}`;
145
+ if (node.tag === 'p')
146
+ return renderInline(node.children);
147
+ if (node.tag === 'blockquote')
148
+ return quote(renderBlocks(node.children));
149
+ if (node.tag === 'pre')
150
+ return fencedCode(node);
151
+ if (node.tag === 'ul' || node.tag === 'ol')
152
+ return renderList(node);
153
+ if (node.tag === 'table')
154
+ return renderTable(node);
155
+ if (node.tag === 'hr')
156
+ return '---';
157
+ if (node.tag === 'br')
158
+ return '';
159
+ if (BLOCK_TAGS.has(node.tag))
160
+ return node.children.some((child) => child.type === 'element' && BLOCK_TAGS.has(child.tag)) ? renderBlocks(node.children) : renderInline(node.children);
161
+ return renderInline(node.children);
162
+ }
163
+ function renderInline(nodes) {
164
+ return nodes.map(renderInlineNode).join('').replace(/\s+/g, ' ').replaceAll(INLINE_BREAK, ' \n').trim();
165
+ }
166
+ function renderInlineNode(node) {
167
+ if (node.type === 'text')
168
+ return markdownText(node.value);
169
+ if (isInteractiveSlot(node) || RAW_TEXT_TAGS.has(node.tag))
170
+ return '';
171
+ const content = renderInline(node.children);
172
+ if (node.tag === 'br')
173
+ return INLINE_BREAK;
174
+ if (isStructuralWrapper(node))
175
+ return content;
176
+ if (node.tag === 'strong' || node.tag === 'b')
177
+ return content === '' ? '' : `**${content}**`;
178
+ if (node.tag === 'em' || node.tag === 'i')
179
+ return content === '' ? '' : `*${content}*`;
180
+ if (node.tag === 'code')
181
+ return codeSpan(textContent(node));
182
+ if (node.tag === 'a') {
183
+ const href = node.attrs.get('href');
184
+ return href === undefined || href === '' || content === '' ? content : `[${content}](${markdownUrl(href)})`;
185
+ }
186
+ if (BLOCK_TAGS.has(node.tag))
187
+ return content;
188
+ // Unknown inline markup is prose, not terminal UI. Keeping its text prevents
189
+ // raw `<span>`-style tags from leaking through termrender.
190
+ return content;
191
+ }
192
+ function renderList(element) {
193
+ const items = element.children.filter((child) => child.type === 'element' && child.tag === 'li');
194
+ return items.map((item, index) => {
195
+ const nested = item.children.filter((child) => child.type === 'element' && (child.tag === 'ul' || child.tag === 'ol'));
196
+ const content = item.children.filter((child) => !(child.type === 'element' && (child.tag === 'ul' || child.tag === 'ol')));
197
+ const marker = element.tag === 'ol' ? `${index + 1}.` : '-';
198
+ const first = renderListItem(content);
199
+ const children = nested.map(renderList).filter(Boolean).map((list) => list.split('\n').map((line) => ` ${line}`).join('\n'));
200
+ return [first === '' ? marker : `${marker} ${first}`, ...children].join('\n');
201
+ }).join('\n');
202
+ }
203
+ function renderListItem(nodes) {
204
+ const blocks = renderBlocks(nodes);
205
+ return blocks.replace(/\n\n+/g, '\n').replace(/\n/g, '\n ');
206
+ }
207
+ function renderTable(table) {
208
+ const rows = findElements(table, 'tr').map((row) => row.children.filter((child) => child.type === 'element' && (child.tag === 'th' || child.tag === 'td')).map((cell) => renderInline(cell.children).replace(/\|/g, '\\|'))).filter((row) => row.length > 0);
209
+ if (rows.length === 0)
210
+ return '';
211
+ const width = Math.max(...rows.map((row) => row.length));
212
+ const normalized = rows.map((row) => Array.from({ length: width }, (_, index) => row[index] ?? ''));
213
+ const header = normalized[0];
214
+ const data = normalized.slice(1);
215
+ return [`| ${header.join(' | ')} |`, `| ${header.map(() => '---').join(' | ')} |`, ...data.map((row) => `| ${row.join(' | ')} |`)].join('\n');
216
+ }
217
+ function fencedCode(pre) {
218
+ const code = pre.children.find((child) => child.type === 'element' && child.tag === 'code');
219
+ const source = textContent(code ?? pre).replace(/\r\n?/g, '\n').replace(/\n$/, '');
220
+ const fence = source.includes('```') ? '~~~' : '```';
221
+ const language = code?.attrs.get('class')?.match(/(?:^|\s)language-([^\s]+)/)?.[1] ?? '';
222
+ return `${fence}${language}\n${source}\n${fence}`;
223
+ }
224
+ function quote(markdown) {
225
+ return markdown.split('\n').map((line) => line === '' ? '>' : `> ${line}`).join('\n');
226
+ }
227
+ function textContent(node) {
228
+ if (node.type === 'text')
229
+ return decodeEntities(node.value);
230
+ return node.children.map(textContent).join('');
231
+ }
232
+ function markdownText(value) {
233
+ return decodeEntities(value)
234
+ .replace(/\\/g, '\\\\')
235
+ .replace(/[<>]/g, (character) => character === '<' ? '&lt;' : '&gt;')
236
+ .replace(/(^|\n)(?=(?:#{1,6}\s|[-+]\s))/g, '$1\\')
237
+ .replace(/(^|\n)(\d+)([.)])(?=\s)/g, '$1$2\\$3')
238
+ .replace(/([\[\]*_`|])/g, '\\$1');
239
+ }
240
+ function codeSpan(value) {
241
+ const source = value.replace(/\r?\n/g, ' ');
242
+ const delimiter = source.includes('`') ? '``' : '`';
243
+ return `${delimiter}${source}${delimiter}`;
244
+ }
245
+ function markdownUrl(value) {
246
+ return `<${decodeEntities(value).replace(/>/g, '%3E')}>`;
247
+ }
248
+ function isStructuralWrapper(node) {
249
+ return node.tag === 'crtr-pages' || node.tag === 'crtr-page';
250
+ }
251
+ function isInteractiveSlot(node) {
252
+ return !isStructuralWrapper(node) && (node.tag === 'crtr-slot' || node.tag.startsWith('crtr-'));
253
+ }
254
+ function isRichBlock(node) {
255
+ if (RAW_TEXT_TAGS.has(node.tag) || RICH_TAGS.has(node.tag) || node.tag.includes('-'))
256
+ return true;
257
+ if (!BLOCK_TAGS.has(node.tag))
258
+ return false;
259
+ return [...node.attrs.keys()].some((attribute) => attribute === 'class' || attribute === 'style' || attribute === 'id' || attribute.startsWith('on'));
260
+ }
261
+ function findElements(node, tag) {
262
+ const found = [];
263
+ for (const child of node.children)
264
+ if (child.type === 'element') {
265
+ if (child.tag === tag)
266
+ found.push(child);
267
+ found.push(...findElements(child, tag));
268
+ }
269
+ return found;
270
+ }
271
+ function decodeEntities(value) {
272
+ return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[0-9a-f]+|\d+);/gi, (entity) => {
273
+ const named = { '&amp;': '&', '&apos;': "'", '&gt;': '>', '&lt;': '<', '&quot;': '"' };
274
+ const lower = entity.toLowerCase();
275
+ if (named[lower] !== undefined)
276
+ return named[lower];
277
+ const numeric = lower.startsWith('&#x') ? Number.parseInt(lower.slice(3, -1), 16) : Number.parseInt(lower.slice(2, -1), 10);
278
+ try {
279
+ return Number.isFinite(numeric) ? String.fromCodePoint(numeric) : entity;
280
+ }
281
+ catch {
282
+ return entity;
283
+ }
284
+ });
285
+ }
286
+ function escapeRegExp(value) {
287
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
288
+ }
@@ -1,6 +1,22 @@
1
+ import type { PageComponentRegistration, ProductPageComponents } from '../../types.js';
1
2
  /** The page slot kinds crtr validates and renders itself. */
2
3
  export declare const BUILTIN_PAGE_KINDS: readonly ["options", "text", "table", "cards", "chart"];
3
- /** Normalize the product-registered page slot catalog from raw config. */
4
- export declare function normalizePageComponents(raw: unknown): string[];
4
+ export declare const STRUCTURAL_PAGE_KINDS: readonly ["crtr-pages", "crtr-page"];
5
+ export interface PageComponentCatalogEntry {
6
+ kind: string;
7
+ tag: string;
8
+ description: string;
9
+ useWhen: string;
10
+ doc?: string;
11
+ builtin: boolean;
12
+ structural: boolean;
13
+ }
14
+ /** Normalize legacy kind strings and documented product component registrations. */
15
+ export declare function normalizePageComponents(raw: unknown): PageComponentRegistration[];
16
+ /** Names-only projection for page validation and rendering consumers. */
17
+ export declare function pageComponentKinds(components: readonly PageComponentRegistration[]): string[];
18
+ /** Full built-in + product catalog for CLI discovery. */
19
+ export declare function pageComponentCatalog(components: readonly PageComponentRegistration[]): PageComponentCatalogEntry[];
5
20
  /** Whether crtr or the product catalog registers a page slot kind. */
6
- export declare function isRegisteredPageKind(kind: string, productKinds: string[]): boolean;
21
+ export declare function productPageComponent(kind: string, components: ProductPageComponents): PageComponentRegistration | undefined;
22
+ export declare function isRegisteredPageKind(kind: string, productComponents: ProductPageComponents): boolean;
@@ -1,7 +1,17 @@
1
1
  /** The page slot kinds crtr validates and renders itself. */
2
2
  export const BUILTIN_PAGE_KINDS = ['options', 'text', 'table', 'cards', 'chart'];
3
+ export const STRUCTURAL_PAGE_KINDS = ['crtr-pages', 'crtr-page'];
3
4
  const PAGE_KIND_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
4
5
  const BUILTIN_PAGE_KIND_SET = new Set(BUILTIN_PAGE_KINDS);
6
+ const BUILTIN_COMPONENTS = [
7
+ { kind: 'crtr-pages', tag: 'crtr-pages', description: 'ordered wrapper for one or more page steps', useWhen: 'use when the page has a response-bearing component or multiple steps', builtin: true, structural: true },
8
+ { kind: 'crtr-page', tag: 'crtr-page', description: 'one step inside a crtr-pages flow', useWhen: 'use for each step the human sees and completes in sequence', builtin: true, structural: true },
9
+ { kind: 'options', tag: 'crtr-options', description: 'known alternatives with single or multiple selection', useWhen: 'use when the human picks among known alternatives, optionally with freetext', builtin: true, structural: false },
10
+ { kind: 'text', tag: 'crtr-text', description: 'editable or read-only text surface', useWhen: 'use when the human writes, edits, or reviews text', builtin: true, structural: false },
11
+ { kind: 'table', tag: 'crtr-table', description: 'rows and columns with optional selection', useWhen: 'use when structured records are easiest to compare in rows and columns', builtin: true, structural: false },
12
+ { kind: 'cards', tag: 'crtr-cards', description: 'visual records with optional selection', useWhen: 'use when the human compares or picks richer items than a compact option list can carry', builtin: true, structural: false },
13
+ { kind: 'chart', tag: 'crtr-chart', description: 'line, bar, or area data visualization', useWhen: 'use when shape, trend, or magnitude matters more than exact tabular values', builtin: true, structural: false },
14
+ ];
5
15
  function describeEntry(entry) {
6
16
  try {
7
17
  const json = JSON.stringify(entry);
@@ -11,33 +21,80 @@ function describeEntry(entry) {
11
21
  return String(entry);
12
22
  }
13
23
  }
14
- /** Normalize the product-registered page slot catalog from raw config. */
24
+ function optionalText(value, field, index) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ if (typeof value !== 'string' || value.trim() === '') {
28
+ throw new Error(`page_components[${index}].${field} must be a non-empty string; received ${describeEntry(value)}`);
29
+ }
30
+ return value.trim();
31
+ }
32
+ /** Normalize legacy kind strings and documented product component registrations. */
15
33
  export function normalizePageComponents(raw) {
16
34
  if (raw === undefined)
17
35
  return [];
18
- if (!Array.isArray(raw)) {
36
+ if (!Array.isArray(raw))
19
37
  throw new Error(`page_components must be an array; received ${describeEntry(raw)}`);
20
- }
21
38
  const seen = new Set();
22
39
  return raw.map((entry, index) => {
23
- if (typeof entry !== 'string') {
24
- throw new Error(`page_components[${index}] must be a string; offending entry: ${describeEntry(entry)}`);
25
- }
26
- const kind = entry.trim();
27
- if (!PAGE_KIND_PATTERN.test(kind)) {
28
- throw new Error(`page_components[${index}] has invalid page kind ${describeEntry(entry)} after trimming; expected /^[a-z][a-z0-9-]{0,63}$/`);
40
+ let registration;
41
+ if (typeof entry === 'string') {
42
+ registration = { kind: entry.trim() };
29
43
  }
30
- if (BUILTIN_PAGE_KIND_SET.has(kind)) {
31
- throw new Error(`page_components[${index}] ${describeEntry(entry)} collides with built-in page kind "${kind}"`);
44
+ else if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) {
45
+ const object = entry;
46
+ const extra = Object.keys(object).filter((key) => !['kind', 'description', 'useWhen', 'doc', 'display'].includes(key));
47
+ if (extra.length > 0)
48
+ throw new Error(`page_components[${index}] has unknown field(s): ${extra.join(', ')}`);
49
+ if (typeof object['kind'] !== 'string')
50
+ throw new Error(`page_components[${index}].kind must be a string; offending entry: ${describeEntry(entry)}`);
51
+ if (object['display'] !== undefined && typeof object['display'] !== 'boolean')
52
+ throw new Error(`page_components[${index}].display must be a boolean; offending entry: ${describeEntry(entry)}`);
53
+ registration = {
54
+ kind: object['kind'].trim(),
55
+ ...(optionalText(object['description'], 'description', index) === undefined ? {} : { description: optionalText(object['description'], 'description', index) }),
56
+ ...(optionalText(object['useWhen'], 'useWhen', index) === undefined ? {} : { useWhen: optionalText(object['useWhen'], 'useWhen', index) }),
57
+ ...(optionalText(object['doc'], 'doc', index) === undefined ? {} : { doc: optionalText(object['doc'], 'doc', index) }),
58
+ ...(object['display'] === true ? { display: true } : {}),
59
+ };
32
60
  }
33
- if (seen.has(kind)) {
34
- throw new Error(`page_components[${index}] ${describeEntry(entry)} duplicates page kind "${kind}"`);
61
+ else {
62
+ throw new Error(`page_components[${index}] must be a string or component object; offending entry: ${describeEntry(entry)}`);
35
63
  }
64
+ const kind = registration.kind;
65
+ if (!PAGE_KIND_PATTERN.test(kind))
66
+ throw new Error(`page_components[${index}] has invalid page kind ${describeEntry(kind)}; expected /^[a-z][a-z0-9-]{0,63}$/`);
67
+ if (BUILTIN_PAGE_KIND_SET.has(kind) || STRUCTURAL_PAGE_KINDS.includes(kind))
68
+ throw new Error(`page_components[${index}] collides with built-in page kind "${kind}"`);
69
+ if (seen.has(kind))
70
+ throw new Error(`page_components[${index}] duplicates page kind "${kind}"`);
36
71
  seen.add(kind);
37
- return kind;
72
+ return registration;
38
73
  });
39
74
  }
75
+ /** Names-only projection for page validation and rendering consumers. */
76
+ export function pageComponentKinds(components) {
77
+ return components.map((component) => component.kind);
78
+ }
79
+ /** Full built-in + product catalog for CLI discovery. */
80
+ export function pageComponentCatalog(components) {
81
+ return [
82
+ ...BUILTIN_COMPONENTS,
83
+ ...components.map((component) => ({
84
+ kind: component.kind,
85
+ tag: component.kind,
86
+ description: component.description ?? 'product-registered page component',
87
+ useWhen: component.useWhen ?? 'use when the product component matches the structured UI the human needs',
88
+ ...(component.doc === undefined ? {} : { doc: component.doc }),
89
+ builtin: false,
90
+ structural: false,
91
+ })),
92
+ ];
93
+ }
40
94
  /** Whether crtr or the product catalog registers a page slot kind. */
41
- export function isRegisteredPageKind(kind, productKinds) {
42
- return BUILTIN_PAGE_KIND_SET.has(kind) || productKinds.includes(kind);
95
+ export function productPageComponent(kind, components) {
96
+ return components.find((component) => component.kind === kind);
97
+ }
98
+ export function isRegisteredPageKind(kind, productComponents) {
99
+ return BUILTIN_PAGE_KIND_SET.has(kind) || productPageComponent(kind, productComponents) !== undefined;
43
100
  }
@@ -1,3 +1,4 @@
1
- import { type PageManifest } from './page-schema.js';
1
+ import type { ProductPageComponents } from '../../types.js';
2
+ import type { PageManifest } from './page-schema.js';
2
3
  /** Render one page ticket's authored document as the runtime's HTML payload. */
3
- export declare function renderPageHtml(manifest: PageManifest, document: string, productKinds?: string[]): string;
4
+ export declare function renderPageHtml(manifest: PageManifest, document: string, productKinds?: ProductPageComponents): string;