@orkestrel/scaffold 0.0.103 → 0.0.105

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 (94) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
  3. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
  4. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
  5. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
  6. package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
  7. package/dist/bin/main.js +17 -10
  8. package/dist/bin/main.js.map +1 -1
  9. package/dist/host/agents/orchestration.md +6 -6
  10. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
  11. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
  13. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
  14. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
  15. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  16. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  17. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
  20. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
  21. package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
  22. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
  23. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
  24. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
  25. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
  26. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
  27. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
  28. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
  29. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
  30. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
  31. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
  32. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +72 -0
  33. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +4 -0
  34. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +12 -0
  35. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
  36. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
  37. package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
  38. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
  39. package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
  40. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
  41. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
  42. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
  43. package/dist/host/agents/transports/codex.md +1 -1
  44. package/dist/host/claude/agents/orkestrel.md +54 -53
  45. package/dist/host/claude/agents/reviewer.md +4 -4
  46. package/dist/host/claude/agents/verifier.md +1 -1
  47. package/dist/host/claude/rules/documentation.md +2 -1
  48. package/dist/host/claude/rules/patterns.md +1 -1
  49. package/dist/host/claude/rules/quality.md +2 -2
  50. package/dist/host/claude/rules/styles.md +1 -1
  51. package/dist/host/claude/rules/tests.md +34 -8
  52. package/dist/host/claude/rules/typescript.md +29 -0
  53. package/dist/host/claude/rules/workspace.md +32 -22
  54. package/dist/host/claude/rules/writing.md +34 -26
  55. package/dist/host/claude/settings.json +2 -0
  56. package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
  57. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
  58. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +11 -0
  59. package/dist/host/codex/agents/analyst.toml +3 -3
  60. package/dist/host/codex/agents/reviewer.toml +4 -4
  61. package/dist/host/codex/agents/verifier.toml +3 -1
  62. package/dist/host/configs/policy.ts +13 -2
  63. package/dist/host/dotfiles/editorconfig +0 -1
  64. package/dist/host/dotfiles/gitattributes +0 -1
  65. package/dist/host/dotfiles/gitignore +0 -1
  66. package/dist/host/dotfiles/oxlintrc.json +24 -0
  67. package/dist/host/guides/agent.md +1674 -598
  68. package/dist/host/guides/budget.md +89 -29
  69. package/dist/host/guides/form.md +485 -99
  70. package/dist/host/guides/mcp.md +14 -2
  71. package/dist/host/guides/ollama.md +367 -86
  72. package/dist/host/guides/probe.md +1 -1
  73. package/dist/host/guides/scaffold.md +158 -101
  74. package/dist/host/guides/study.md +3185 -0
  75. package/dist/host/guides/table.md +239 -29
  76. package/dist/host/guides/terminal.md +22 -20
  77. package/dist/host/guides/test.md +776 -315
  78. package/dist/host/manifest.json +117 -65
  79. package/dist/host/tests/config.test.ts +136 -82
  80. package/dist/host/tests/policy.test.ts +41 -3
  81. package/dist/host/tests/setupPolicy.ts +87 -29
  82. package/dist/src/core/index.cjs +1389 -612
  83. package/dist/src/core/index.cjs.map +1 -1
  84. package/dist/src/core/index.d.cts +35 -20
  85. package/dist/src/core/index.d.ts +35 -20
  86. package/dist/src/core/index.js +1387 -613
  87. package/dist/src/core/index.js.map +1 -1
  88. package/dist/src/server/index.cjs +12 -12
  89. package/dist/src/server/index.cjs.map +1 -1
  90. package/dist/src/server/index.d.cts +12 -12
  91. package/dist/src/server/index.d.ts +12 -12
  92. package/dist/src/server/index.js +12 -12
  93. package/dist/src/server/index.js.map +1 -1
  94. package/package.json +5 -5
@@ -4,16 +4,15 @@
4
4
  > holding the rows given against it, and one lens of sort, filter, and page deciding which of them
5
5
  > the view shows.
6
6
 
7
- Nothing here renders, measures a pixel, reads a keyboard, or names a host type. **A grid, a report,
7
+ The core renders nothing and names no host type. **A grid, a report,
8
8
  a terminal listing, and a CSV export are the same abstraction.** They all hold a set of records,
9
9
  order them, narrow them, and show a stretch of them. What differs is who draws the result, and
10
- drawing is the one part this package leaves out. The table owns values; the host owns everything a
11
- person looks at.
10
+ drawing belongs to each host. The browser face binds native table elements to that same document.
12
11
 
13
12
  The row store is the source of truth, and it is the whole of it. Sort terms, filters, the picked
14
13
  keys, the opened keys, and the page are held; `view` and every tally are worked out on read, so no
15
14
  second copy of an answer can go stale. A write validates all of itself before any of it lands, and
16
- announces itself once it has.
15
+ announces itself after it has.
17
16
 
18
17
  The core refuses rather than throws. Every guard returns `false` off-shape rather than throwing,
19
18
  every parser returns `undefined` on refusal, and every row the table hands back is a frozen owned
@@ -21,7 +20,8 @@ copy. Table-owned refusals raise `TableError`, and each one names a caller mista
21
20
 
22
21
  ## Surface
23
22
 
24
- Everything in this guide is exported from `@orkestrel/table` ([`src/core`](../src/core)). The manager
23
+ The core surface is exported from `@orkestrel/table` ([`src/core`](../src/core)); the native binding
24
+ surface comes from `@orkestrel/table/browser` ([`src/browser`](../src/browser)). The manager
25
25
  classes and the key-set shell they compose are the module's internal declarations: they stay out
26
26
  of the barrel, so no consumer can construct one, and each constructor takes the table's emitter
27
27
  and closures over state `Table` keeps private, which is why they are internal rather than
@@ -63,6 +63,208 @@ table.pagination.count // 2 — two pages of two
63
63
  table.view.map((row) => row.name) // ['Grace', 'Alan'] — page one, oldest first
64
64
  ```
65
65
 
66
+ ### Native binding
67
+
68
+ These declarations belong to the browser entry.
69
+
70
+ | API | Kind | Shape | Summary |
71
+ | ----------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
72
+ | `TableBinding` | class | `TableBindingInterface` | Retains keyed native rows while projecting every core table manager. |
73
+ | `TableBindingInterface` | interface | `{ element, document, destroyed, emitter } plus refresh, destroy` | Binds native table elements to a borrowed core document. |
74
+ | `TableBindingOptions` | interface | `{ row?, detail?, scope?, describe?: { range?, sort? }, on?, error? }` | Configures row ownership, detail presentation, scoped controls, and event listeners. |
75
+ | `TableRange` | interface | `{ first, last, count, page, pages, limit }` | Describes the displayed page within the filtered rows. |
76
+ | `TableSortDescription` | interface | `{ column, direction, priority, count }` | Describes a column's direction and one-based priority among active sort orders. |
77
+ | `TableExpansion` | interface | `{ key, expanded }` | Proposes one row's next expansion state in a cancelable expand request. |
78
+ | `TableBindingEventMap` | type | `{ sort: readonly [event: CustomEvent<readonly TableOrder[]>]; expand: readonly [event: CustomEvent<readonly TableExpansion[]>]; render: readonly []; error: readonly [error: unknown] }` | Announces cancelable sorting and expanding, completed rendering, and failed DOM operations. |
79
+ | `TableRenderHandler` | type | `(element: HTMLTableRowElement, row: TableRow) => void` | Patches the contents of a retained row without moving or replacing that row. |
80
+ | `TableProjection` | interface | `{ schema, rows }` | Holds a validated native table projection for constructing a core document. |
81
+ | `TableBindingErrorCode` | type | `'STRUCTURE'` | Names a native binding failure at its structure, rendering, or lifecycle boundary. |
82
+ | `TableBindingError` | class | `new (code: TableBindingErrorCode, message: string) => TableBindingError` | Represents a native table binding failure with a machine-readable code. |
83
+ | `isTableBindingError` | function | `(value: unknown) => value is TableBindingError` | Checks whether a caught value is a native table binding error. |
84
+ | `createTableBinding` | function | `(element: HTMLTableElement, document: TableInterface, options?: TableBindingOptions) => TableBindingInterface` | Creates a native table binding with one retained row owner. |
85
+ | `readTable` | function | `(element: HTMLTableElement) => TableProjection` | Reads declared header metadata and explicit row keys into a validated core projection. |
86
+ | `readTableSchema` | function | `(element: HTMLTableElement) => TableSchema` | Reads and validates explicit native header metadata. |
87
+ | `readTableRows` | function | `(schema: TableSchema, elements: readonly HTMLTableRowElement[]) => readonly TableRow[]` | Reads explicit keys and declared direct cells against a core schema. |
88
+
89
+ ### Enhance authored rows
90
+
91
+ Declare one body, a table identity-column name, explicit header column keys and cell kinds, and a
92
+ stable key on every data row. Direct cells declare their columns; omitted cells stay absent. Text
93
+ is preserved verbatim. Numbers and flags use the core parser. A native data element's value or a
94
+ time element's datetime takes precedence over cell text. Choice headers carry their explicit
95
+ choice array as JSON in data-table-choices. Nested tables supply no headers or controls to their parent.
96
+
97
+ This browser example constructs the document before binding it.
98
+
99
+ ```ts
100
+ import { createTable } from '@orkestrel/table'
101
+ import { createTableBinding, readTable } from '@orkestrel/table/browser'
102
+
103
+ const element = document.createElement('table')
104
+ element.setAttribute('data-table-key', 'id')
105
+ element.innerHTML =
106
+ '<caption>People</caption><thead><tr><th data-table-column="id" data-table-cell="text">Reference</th><th data-table-column="name" data-table-cell="text"><button data-table-sort="name">Name</button></th></tr></thead><tbody><tr data-table-key="ash"><td data-table-column="name">Ash</td></tr><tr data-table-key="cedar"><td data-table-column="name">Cedar</td></tr></tbody>'
107
+ document.body.append(element)
108
+ const projection = readTable(element)
109
+ const table = createTable(projection.schema, { rows: projection.rows })
110
+ const binding = createTableBinding(element, table)
111
+ table.filter.set({ column: 'name', operator: 'contains', text: 'Ash' })
112
+ binding.refresh()
113
+ element.tBodies[0]?.rows.length // 1
114
+ binding.destroy()
115
+ element.tBodies[0]?.rows.length // 2: restored in core store order
116
+ table.destroyed // false
117
+ element.remove()
118
+ ```
119
+
120
+ The binding validates the entire authored read before changing core rows. External inserts,
121
+ removals, and cell edits reach the document; an invalid read emits error and writes nothing.
122
+ Detached rows remain observed while their keys live. Authored mode owns cell values in the DOM;
123
+ use the core managers for the view, and edit authored cells to change values. Model mode owns
124
+ values in the document and requires an empty body and a row callback. That callback receives
125
+ the retained row first and its value second. It only patches contents, and runs on creation or
126
+ value change. Sort, selection, filtering, expansion, and pagination do not rerun it.
127
+
128
+ A detail callback receives a generated row containing one spanning cell. Patch that cell's
129
+ contents; the binding owns its placement and identity. Collapsed and off-view details are retained
130
+ until their data key leaves the store. Render callbacks must return synchronously. A throw emits
131
+ error without render success; refresh retries. Arbitrary callback effects are not rolled back.
132
+
133
+ Native hooks use real controls: a header button with data-table-sort naming the column, a row
134
+ checkbox with data-table-select, a row button with data-table-expand, and a scope checkbox with
135
+ data-table-page. Scope defaults to the table. Use a scope dedicated to that table for an external
136
+ controls. It selects or clears only current view keys; off-page selection stays selected.
137
+ Sort clicks cycle ascending, descending, and absent, replacing the primary order. Programmatic
138
+ multi-sort remains intact and is described on each sorted header button. The sort event carries
139
+ a cancelable CustomEvent whose detail is the proposed order array; preventDefault vetoes before
140
+ the document changes. A row expand button or the page expand button emits expand first, with a
141
+ cancelable CustomEvent whose detail lists each key and its proposed expanded state; preventDefault
142
+ leaves every key and row unchanged, so a host can run its own close transition and then clear the
143
+ key through the document's expansion. The core managers remain the only owners of filters and paging.
144
+
145
+ An external pager belongs inside the binding's dedicated scope. Use a select with
146
+ `data-table-limit`, buttons with `data-table-move="previous"` and `data-table-move="next"`, and an
147
+ authored `output` with `data-table-range`. A limit option's value is a positive integer; an empty
148
+ value removes paging. Invalid values emit a binding error without changing pagination. Programmatic
149
+ limit changes update the select; include an option for each limit your page uses. Native disabled
150
+ states follow the bounds. A focused button that becomes disabled hands focus to the enabled
151
+ opposite button, or to the named table when neither direction is available.
152
+
153
+ The range output is cleared before filling on a later task. It fills initially and when the
154
+ displayed range, filtered count, page, page count, or limit changes. Sorting, selection, expansion,
155
+ and refresh alone don't repeat the sentence. Its implicit status role needs no added live role.
156
+ The default is `1 to 2 of 3`, or `0 to 0 of 0` for an empty view. The `describe.range` callback
157
+ receives one-based bounds, filtered count, page and page count, and the optional page limit.
158
+ The `describe.sort` callback receives the column, direction, one-based priority, and active order
159
+ count. Its default is `Sort priority 2 of 2, descending`. Formatter throws emit error without
160
+ render success; refresh retries. Destroy cancels pending fills and restores control values still
161
+ carrying the binding's last write, preserving caller edits.
162
+
163
+ A button with `data-table-expand="page"` expands the visible keys, or collapses them when every
164
+ visible key is expanded. Off-page expansion stays intact. The cancelable `expand` request lists
165
+ only the keys whose state changes. The label stays authored;
166
+ `aria-expanded` carries the state, and an empty view disables the button. Row controls continue
167
+ to use bare `data-table-expand`. Detail rows and their identifiers survive collapse and paging.
168
+ Nested tables supply none of these controls to the containing binding. The binding renders no
169
+ numbered page window; presentation classes belong to the host.
170
+
171
+ This scoped binding example customizes wording while retaining native control labels.
172
+
173
+ ```ts
174
+ import { createTable } from '@orkestrel/table'
175
+ import { createTableBinding, readTable } from '@orkestrel/table/browser'
176
+
177
+ const scope = document.createElement('section')
178
+ scope.innerHTML =
179
+ '<table data-table-key="id"><caption>Accounts</caption><thead><tr><th data-table-column="id" data-table-cell="text"><button data-table-sort="id">Account</button></th></tr></thead><tbody><tr data-table-key="ash"><td data-table-column="id">ash</td></tr><tr data-table-key="cedar"><td data-table-column="id">cedar</td></tr></tbody></table><nav aria-label="Account pages"><output data-table-range></output><select aria-label="Rows per page" data-table-limit><option value="1">1</option><option value="">All</option></select><button type="button" data-table-move="previous">Previous</button><button type="button" data-table-move="next">Next</button><button type="button" data-table-expand="page">Page details</button></nav>'
180
+ document.body.append(scope)
181
+ const element = scope.querySelector('table')
182
+ if (element === null) throw new Error('Missing account table')
183
+ const projection = readTable(element)
184
+ const table = createTable(projection.schema, { rows: projection.rows, limit: 1 })
185
+ const binding = createTableBinding(element, table, {
186
+ scope,
187
+ describe: {
188
+ range: ({ first, last, count }) => `${first}–${last} of ${count} accounts`,
189
+ sort: ({ priority, count, direction }) => `Order ${priority}/${count}: ${direction}`,
190
+ },
191
+ detail: (node, row) => {
192
+ const cell = node.cells[0]
193
+ if (cell !== undefined) cell.textContent = `Account ${row['id']}`
194
+ },
195
+ })
196
+ // Keep the binding while the page is mounted; release it before removing the scope.
197
+ binding.destroy()
198
+ table.destroy()
199
+ scope.remove()
200
+ ```
201
+
202
+ Rendering coalesces on a later task, including writes during callbacks. Rows and child controls
203
+ retain identity across the view. Reorders use native state-preserving movement when available;
204
+ focus is restored on fallback moves. When a focused row leaves the view, focus moves to the named
205
+ table. An already focused external control keeps focus. Destroy disconnects scheduling, observers,
206
+ and listeners, restores attributes still carrying the binding's values, removes details, and
207
+ returns retained authored rows in core order while retaining a connected focused control. A caller's
208
+ deliberate focus choice outside the row remains untouched. Model rows remain an inert final snapshot. The
209
+ borrowed document stays live.
210
+
211
+ The lower-level readers share the same validation boundary, and the row comparison ignores
212
+ snapshot identity.
213
+
214
+ ```ts
215
+ import {
216
+ TableBinding,
217
+ TableBindingError,
218
+ isTableBindingError,
219
+ readTableRows,
220
+ readTableSchema,
221
+ } from '@orkestrel/table/browser'
222
+ import { createTable, matchesTableRow } from '@orkestrel/table'
223
+
224
+ const element = document.createElement('table')
225
+ element.setAttribute('data-table-key', 'id')
226
+ element.innerHTML =
227
+ '<thead><tr><th data-table-column="id" data-table-cell="text">Reference</th></tr></thead><tbody></tbody>'
228
+ const schema = readTableSchema(element)
229
+ const rows = readTableRows(schema, []) // []
230
+ const table = createTable(schema, { rows })
231
+ const binding = new TableBinding(element, table)
232
+ binding.destroy()
233
+ matchesTableRow({ id: 'ash' }, { id: 'ash' }) // true
234
+ isTableBindingError(new TableBindingError('STRUCTURE', 'Missing identity')) // true
235
+ ```
236
+
237
+ ### Replace complete rows
238
+
239
+ Replacement removes omitted optional cells while preserving the identity and its manager state.
240
+
241
+ ```ts
242
+ import { createTable } from '@orkestrel/table'
243
+
244
+ const table = createTable(
245
+ {
246
+ key: 'id',
247
+ columns: [
248
+ { key: 'id', cell: 'text' },
249
+ { key: 'name', cell: 'text' },
250
+ ],
251
+ },
252
+ { rows: [{ id: 'ash', name: 'Ash' }] },
253
+ )
254
+ table.selection.select('ash')
255
+ table.rows.replace({ id: 'ash' }) // true
256
+ table.rows.row('ash') // { id: 'ash' }
257
+ table.selection.keys.has('ash') // true
258
+ ```
259
+
260
+ ### Row equality
261
+
262
+ This helper belongs to the core entry.
263
+
264
+ | API | Kind | Shape | Summary |
265
+ | ----------------- | -------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
266
+ | `matchesTableRow` | function | `(left: TableRow \| undefined, right: TableRow) => boolean` | Compares complete row values independently of their snapshot identities. |
267
+
66
268
  ### Rows, cells, and columns
67
269
 
68
270
  The document itself — what a table declares and what one row of it holds. All data, no behavior.
@@ -116,7 +318,7 @@ A `Shape` cell holds an interface's data members as bare names in braces, `?` ma
116
318
  | `createTable` | function | `(schema: TableSchema, options?: TableOptions) => TableInterface` | Opens a table against a schema. The schema is copied, and the copy is what the table declares. |
117
319
  | `TableOptions` | interface | `{ on?, error?, rows?, comparators?, matchers?, limit? }` | Describes how to open a table — the listeners wired at construction and where a throw from one goes, the rows seeded into it, the per-column comparison and test replacements, and the page size. |
118
320
  | `TableEventMap` | type | `{ write, remove, sort, filter, select, expand, paginate, clear }` | Lists everything a table announces, mapping each event to the payload its listeners receive. |
119
- | `RowManagerInterface` | interface | `{} plus row, rows, add, update, move, remove` | Manages the rows a table holds, in the order it holds them. |
321
+ | `RowManagerInterface` | interface | `{} plus row, rows, add, update, replace, move, remove` | Manages the rows a table holds, in the order it holds them. |
120
322
  | `SortManagerInterface` | interface | `{} plus order, orders, set, remove` | Manages the order a table reads its rows in. |
121
323
  | `FilterManagerInterface` | interface | `{} plus filter, filters, set, remove` | Manages which rows a table keeps. |
122
324
  | `SelectionManagerInterface` | interface | `{ keys } plus select, clear, toggle` | Manages the rows somebody has picked. |
@@ -160,7 +362,7 @@ A `Shape` cell holds the constant's declared type.
160
362
  ### Guards
161
363
 
162
364
  Total `is*` guards over unknown input. None throws, none coerces, and each returns `false` for
163
- anything off-shape — including a hostile prototype, a symbol key, or a cyclic value.
365
+ anything off-shape — including an unexpected prototype, a symbol key, or a cyclic value.
164
366
 
165
367
  In a guard table a `Shape` cell holds the type the guard narrows to.
166
368
 
@@ -202,7 +404,7 @@ lens-list operations, the cell gate, the comparison, the filter tests, the row p
202
404
  audit, and the wire projections. `computeKeys`, `matchesTerms`, `filterRows`, and `sortRows`
203
405
  propagate exceptions from supplied callbacks; `serializeTable` raises `SCHEMA` for a `meta` no clone
204
406
  can own. The other helpers are total over ordinary declared inputs, subject to the core's
205
- hostile-reflection boundary described later.
407
+ reflection-failure boundary described later.
206
408
 
207
409
  | API | Kind | Summary |
208
410
  | ---------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
@@ -233,7 +435,7 @@ row the table hands back is a live internal reference.
233
435
  | `cloneRow` | function | Clones one row into an owned frozen snapshot. |
234
436
  | `cloneSchema` | function | Clones a whole schema into an owned frozen snapshot, freezing every nested column, choice list, choice, and `meta`; raises `SCHEMA` for a `meta` it cannot own. |
235
437
 
236
- `cloneRow` cannot fail for an ordinary row record, subject to the core's hostile-reflection boundary
438
+ `cloneRow` cannot fail for an ordinary row record, subject to the core's reflection-failure boundary
237
439
  described later. `cloneSchema` can also fail when a column's `meta` holds something no clone can own, such as a
238
440
  record that refers back to itself. `meta` is typed as JSON and a cycle satisfies that type, so the
239
441
  refusal is a `TableError` coded `SCHEMA` rather than a silent partial copy. `createTable` never reaches
@@ -253,9 +455,9 @@ owned value rather than the caller's.
253
455
  | `parseTable` | function | Parses unknown wire data into an owned, structurally valid, semantically sound table schema. |
254
456
  | `parseRows` | function | Parses unknown wire data into owned rows against one table schema, coercing a numeric string and `'true'` / `'false'`. |
255
457
 
256
- **Hostile reflection.** A TypeScript shape does not guarantee that reflection succeeds. An ordinary
458
+ **Reflection failures.** A TypeScript shape does not guarantee that reflection succeeds. An ordinary
257
459
  helper or cloner may propagate a throw from a proxy trap or accessor. Guards and parsers contain that
258
- throw and refuse instead. This is the hostile-reflection boundary for the whole core.
460
+ throw and refuse instead. This is the reflection-failure boundary for the whole core.
259
461
 
260
462
  ## Cells
261
463
 
@@ -780,7 +982,7 @@ the filter admits, so nothing here can drift out of step with what the table hol
780
982
  "page 1 of 1" over an empty list needs no separate case for it.
781
983
  - A page beyond the last one clamps to the last one. Narrowing the filter therefore moves the page
782
984
  on its own, and moving to page 9 of 3 shows page 3 rather than nothing.
783
- - `resize` keeps the first row the view was showing, so the page moves to wherever that row now
985
+ - `resize` keeps the first row the view was showing, so the page moves to wherever that row
784
986
  falls. Resizing is a change of magnification, not a jump to the top.
785
987
 
786
988
  ```ts
@@ -942,11 +1144,11 @@ An event fires only when something actually moved: a write that changes nothing
942
1144
  | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
943
1145
  | `write` | the row's `key` | Once per row admitted, written over, or moved to a different place, in the order the table wrote them. A listener that wants the row reads it back. |
944
1146
  | `remove` | the row's `key` | Once per row taken out, in the order the table removed them. A `clear` is the exception; see its row. |
945
- | `sort` | every current `TableOrder` | Whenever the term list's content changes, carrying the whole list as it now stands. |
946
- | `filter` | every current `TableFilter` | Whenever the filter list's content changes, carrying the whole list as it now stands. |
1147
+ | `sort` | every current `TableOrder` | Whenever the term list's content changes, carrying the whole list as it stands. |
1148
+ | `filter` | every current `TableFilter` | Whenever the filter list's content changes, carrying the whole list as it stands. |
947
1149
  | `select` | the picked `TableKey` set | Whenever the picked set changes, carrying the whole set — including when removing rows pruned it. |
948
1150
  | `expand` | the opened `TableKey` set | Whenever the opened set changes, carrying the whole set — including when removing rows pruned it. |
949
- | `paginate` | the `page` now shown | Whenever the paged window moves: a `move`, a `resize`, and a clamp caused by a narrower filter or by rows leaving. |
1151
+ | `paginate` | the `page` shown | Whenever the paged window moves: a `move`, a `resize`, and a clamp caused by a narrower filter or by rows leaving. |
950
1152
  | `clear` | nothing | On a completed `clear`. It is the whole announcement of that reset: no `remove`, no `sort`, no `filter`, no `select`, no `expand`, and no `paginate` is emitted beside it. |
951
1153
 
952
1154
  A payload is owned by the listener. `sort` and `filter` carry the whole axis because a single term is
@@ -1163,7 +1365,7 @@ a host can check against the same number the package checks against.
1163
1365
  They bind at the schema door and the value door, and which door a limit sits at is the whole story.
1164
1366
 
1165
1367
  **The schema door reports.** `auditTable` counts columns, choices, names, strings, total text, and
1166
- total nodes — `meta` included, since it is retained like everything else — and returns one human
1368
+ total nodes — `meta` included, because it is retained like everything else — and returns one human
1167
1369
  diagnostic per breach. `createTable` raises `SCHEMA` carrying them and `parseTable` refuses the
1168
1370
  schema, so no over-budget schema is ever held.
1169
1371
 
@@ -1257,14 +1459,22 @@ it either way.
1257
1459
 
1258
1460
  #### `RowManagerInterface`
1259
1461
 
1260
- | Method | Returns | Summary |
1261
- | -------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1262
- | `row` | `TableRow` or `undefined` | Finds one row by key; `undefined` when the table holds no such key. |
1263
- | `rows` | `readonly TableRow[]` | Reads every row the table holds, in its own order — unfiltered, unsorted, and unpaged. |
1264
- | `add` | `void` | Takes in one row or several, appending them in the order given. Every row is checked before any is admitted. |
1265
- | `update` | `boolean` | Writes over one row or several, each found by the key it carries. The cells given replace; the cells left out stay. |
1266
- | `move` | `boolean` | Moves one row to another place in the table's own order, counted from zero and clamped to the rows that exist. |
1267
- | `remove` | `void` or `boolean` | Takes out every row, one row, or several. Selection and expansion drop the keys of the rows that went. |
1462
+ | Method | Returns | Summary |
1463
+ | --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1464
+ | `row` | `TableRow` or `undefined` | Finds one row by key; `undefined` when the table holds no such key. |
1465
+ | `rows` | `readonly TableRow[]` | Reads every row the table holds, in its own order — unfiltered, unsorted, and unpaged. |
1466
+ | `add` | `void` | Takes in one row or several, appending them in the order given. Every row is checked before any is admitted. |
1467
+ | `update` | `boolean` | Writes over one row or several, each found by the key it carries. The cells given replace; the cells left out stay. |
1468
+ | `replace` | `boolean` | Replaces complete rows under existing keys, removing omitted cells without moving identities. |
1469
+ | `move` | `boolean` | Moves one row to another place in the table's own order, counted from zero and clamped to the rows that exist. |
1470
+ | `remove` | `void` or `boolean` | Takes out every row, one row, or several. Selection and expansion drop the keys of the rows that went. |
1471
+
1472
+ #### `TableBindingInterface`
1473
+
1474
+ | Method | Returns | Summary |
1475
+ | --------- | ------- | --------------------------------------------------------------------------------- |
1476
+ | `refresh` | `void` | Reconciles and renders the document, retrying a failed renderer. |
1477
+ | `destroy` | `void` | Releases listeners and restores owned attributes without destroying the document. |
1268
1478
 
1269
1479
  #### `SortManagerInterface`
1270
1480
 
@@ -1372,7 +1582,7 @@ table.count // 1
1372
1582
  These invariants hold across [`src/core`](../src/core) and this guide.
1373
1583
 
1374
1584
  1. **Documented surface equals exported surface.** Every row in the `## Surface` tables is a real
1375
- barrel export of `src/core`, and every barrel export is a row — both directions, exhaustively.
1585
+ barrel export of `src/core` or `src/browser`, and every barrel export is a row — both directions, exhaustively.
1376
1586
  The manager classes and the key-set shell are internal, so the parity suite's internal list names
1377
1587
  them and nothing else.
1378
1588
  2. **Documented methods equal interface methods.** Each `## Methods` table lists exactly its
@@ -1432,7 +1642,7 @@ These invariants hold across [`src/core`](../src/core) and this guide.
1432
1642
  whatever it reset, so a reset of ten thousand rows is one event. `destroy` called twice does what
1433
1643
  calling it once did; afterwards every write raises `DESTROYED` while every getter still answers
1434
1644
  what the table last held, and `destroyed` reports the fact so a host need not catch to learn it.
1435
- 16. **Guards are total and parsers refuse.** No `is*` throws for any input — hostile prototype,
1645
+ 16. **Guards are total and parsers refuse.** No `is*` throws for any input — unexpected prototype,
1436
1646
  symbol key, cycle, or depth. No `parse*` throws; each returns `undefined` on refusal. A
1437
1647
  guard-valid value is never refused by its parser, and every parsed result satisfies its guard.
1438
1648
  `isTableSchema` is therefore semantic and not merely structural: it is the exact shape plus an
@@ -1491,7 +1701,7 @@ this package answers today through a mechanism it already exposes.
1491
1701
  | Grouping and aggregation | out | Group headers, subtotals, and rollups add a second row kind that is not a row, and a fold that is not a filter. `view` stays a flat list of the rows the table holds. |
1492
1702
  | Tree and hierarchical rows | out | A parent key would make `view` a traversal rather than a projection, and expansion would mean "show children" instead of "this row is open". Expansion here says only which rows are open. |
1493
1703
  | Editing transactions | out | A staged edit set with commit and rollback is a second store beside the row store, and two writers that can disagree. A host that needs one holds the pending values and calls `update` once. |
1494
- | Undo and history | out | The table holds the rows now. A stack of everything before is a host concern with a host's retention policy, built over the same `update` and `remove` calls. |
1704
+ | Undo and history | out | The table holds the rows. A stack of everything before is a host concern with a host's retention policy, built over the same `update` and `remove` calls. |
1495
1705
  | Async data sources | host | Every read here is synchronous, so `view` is right the instant anything moves. Fetching, paging a server, and reconciling a response are the host's; it reads the lens and calls `add` or `clear`. |
1496
1706
  | Server-side paging | seam | `sort.orders()`, `filter.filters()`, `pagination.offset`, and `pagination.limit` are already the shape a query asks for, in the vocabulary `@orkestrel/database` uses. |
1497
1707
  | Row count budget | out | A table legitimately holds a million rows. The budgets bound one schema's declarations, which arrive from a wire; how many rows a host holds is the host's memory to spend. |
@@ -1503,7 +1713,7 @@ this package answers today through a mechanism it already exposes.
1503
1713
  | CSV and spreadsheet export | host | `view` or `rows()` plus `schema.columns` is the whole input an exporter needs, and the file format, encoding, and download belong to the host that has a filesystem or a browser. |
1504
1714
  | Cell-level selection | out | Selection holds row keys. A rectangular cell range is a second selection model with its own vocabulary, and no consumer has asked for one. |
1505
1715
  | Choice `meta` | out | `meta` is on `ColumnBase` alone, because a column carrier is what the first consumer asked for. The exact guard refuses it on `ColumnChoice` until one asks. |
1506
- | Browser binding | `src/browser` | Binding a table to real elements — a grid, its headings, its scroll container — belongs in a future `src/browser`, taking a table and an element. Nothing renders in this round. |
1716
+ | Browser binding | `src/browser` | The browser entry binds native table rows and controls to a borrowed core document; the core remains host-independent. |
1507
1717
 
1508
1718
  ## Tests
1509
1719
 
@@ -1536,7 +1746,7 @@ this package answers today through a mechanism it already exposes.
1536
1746
  `compareCells`, `admitsFilter`, `matchesFilter`, `filterRows`, `sortRows`, `auditTable`,
1537
1747
  `serializeTable`, `serializeRows`, and the budgets.
1538
1748
  - [`tests/src/core/validators.test.ts`](../tests/src/core/validators.test.ts) — every guard against
1539
- valid, off-shape, and hostile input, plus guard/parser soundness in both directions.
1749
+ valid, off-shape, and untrusted input, plus guard/parser soundness in both directions.
1540
1750
  - [`tests/src/core/parsers.test.ts`](../tests/src/core/parsers.test.ts) — `parseTable`, `parseRows`,
1541
1751
  the coercions, identity at the parse door, and the canonical byte-stable round trip.
1542
1752
  - [`tests/src/core/cloners.test.ts`](../tests/src/core/cloners.test.ts) — every clone is owned,
@@ -351,23 +351,23 @@ A `Shape` cell holds an interface's data members as bare names in braces, `?` ma
351
351
  The stream guards, the cursor math behind the in-place re-render, and the per-field line projections
352
352
  the walk renders with ([`src/server`](../src/server)). All pure, all exported, all unit-tested.
353
353
 
354
- | API | Kind | Summary |
355
- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
356
- | `isInputStream` | function | Checks whether `value` is a usable `InputStreamInterface` — a record with callable `on` / `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`, or a fake TTY a test injects) to the exact slice the driver reads, never an assertion. |
357
- | `isReadable` | function | Checks whether `value` is a Node `NodeJS.ReadableStream` — a total structural guard checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface` requires as its `input`. The non-TTY fallback narrows the resolved input stream through this before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns `false` for a minimal fake that isn't a full readable. |
358
- | `supportsRawMode` | function | Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and exposes a callable `setRawMode`. The `Terminal` probes this to choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a live re-render; false selects the `node:readline` line-input fallback, because a piped or non-terminal stream cannot enter raw mode. Total — never throws. |
359
- | `lineCount` | function | Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of the in-place re-render: the driver records the line count of the view it wrote so the next redraw knows how far up to move the cursor before overwriting. Total; an empty string is one empty line. |
360
- | `renderCursorUp` | function | Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`), or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A` is a wasted write. The pure step the in-place re-render uses to climb back over the previous view before clearing it. Total. |
361
- | `redrawPrefix` | function | Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place — given the line count of the previous view, it moves the cursor up over those lines, returns it to column 0, and erases everything from there to the end of the screen, so the next view is drawn on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes this immediately followed by the new view. |
362
- | `fieldToText` | function | Projects any field the walk reads as a line of text into the `TextField` the text reducer takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`, `datetime`, `color`, and one `file` entry. The label carries that control's format cue from `CONTROL_HINTS`, and a declared `default` becomes the line a bare return submits. The projection carries no rule, because the authoritative form still evaluates the answer this line binds; it exists only so one reducer covers every one of them. |
363
- | `valueToText` | function | Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders as nothing, because a locked field nobody has answered has nothing to show. |
364
- | `filterEnabled` | function | Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled choice's value at every door, including a fill, so the walk never puts one in front of the cursor. Pair with `filterDisabled` to tell the reader what was withheld. |
365
- | `filterDisabled` | function | Returns the choices a `select` or `checkbox` field shows but refuses — the complement of `filterEnabled`, rendered by `renderUnavailableLine` above the list so a reader sees why a declared choice is missing from it. |
366
- | `renderGroupHeader` | function | Renders the section header the walk writes when it enters a new field group, painted by the `message` role. |
367
- | `renderLockedLine` | function | Renders the read-only line a locked field shows — its label, the `LOCKED_MARK`, and the answer the form already holds. The walk writes this instead of a prompt, because the field is still validated and still submitted but must not be edited here. |
368
- | `renderSuggestionLine` | function | Renders the line listing an open select's offered values above its text prompt — a suggestion list, because an open select admits an answer the list does not offer. |
369
- | `renderUnavailableLine` | function | Renders the line naming the choices a field shows but refuses, written above the list the walk drives. |
370
- | `renderNumberedList` | function | Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate with arrow keys, so each offered choice is printed with the number the reader types back. One line per choice, with no trailing newline. |
354
+ | API | Kind | Summary |
355
+ | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
356
+ | `isInputStream` | function | Checks whether `value` is a usable `InputStreamInterface` — a record with callable `on` / `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`, or a fake TTY a test injects) to the exact slice the driver reads, never an assertion. |
357
+ | `isReadable` | function | Checks whether `value` is a Node `NodeJS.ReadableStream` — a total structural guard checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface` requires as its `input`. The non-TTY fallback narrows the resolved input stream through this before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns `false` for a minimal fake that isn't a full readable. |
358
+ | `supportsRawMode` | function | Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and exposes a callable `setRawMode`. The `Terminal` probes this to choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a live re-render; false selects the `node:readline` line-input fallback, because a piped or non-terminal stream cannot enter raw mode. Total — never throws. |
359
+ | `lineCount` | function | Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of the in-place re-render: the driver records the line count of the view it wrote so the next redraw knows how far up to move the cursor before overwriting. Total; an empty string is one empty line. |
360
+ | `renderCursorUp` | function | Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`), or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A` is a wasted write. The pure step the in-place re-render uses to climb back over the previous view before clearing it. Total. |
361
+ | `redrawPrefix` | function | Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place — given the line count of the previous view, it moves the cursor up over those lines, returns it to column 0, and erases everything from there to the end of the screen, so the next view is drawn on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes this immediately followed by the new view. |
362
+ | `fieldToText` | function | Projects any field the walk reads as a line of text into the `TextField` the text reducer takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`, `datetime`, `color`, one `list` entry, and one `file` entry. The label carries that control's format cue from `CONTROL_HINTS`, and a declared `default` becomes the line a bare return submits. The projection carries no rule, because the authoritative form still evaluates the answer this line binds; it exists only so one reducer covers every one of them. |
363
+ | `valueToText` | function | Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders as nothing, because a locked field nobody has answered has nothing to show. |
364
+ | `filterEnabled` | function | Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled choice's value at every door, including a fill, so the walk never puts one in front of the cursor. Pair with `filterDisabled` to tell the reader what was withheld. |
365
+ | `filterDisabled` | function | Returns the choices a `select` or `checkbox` field shows but refuses — the complement of `filterEnabled`, rendered by `renderUnavailableLine` above the list so a reader sees why a declared choice is missing from it. |
366
+ | `renderGroupHeader` | function | Renders the section header the walk writes when it enters a new field group, painted by the `message` role. |
367
+ | `renderLockedLine` | function | Renders the read-only line a locked field shows — its label, the `LOCKED_MARK`, and the answer the form already holds. The walk writes this instead of a prompt, because the field is still validated and still submitted but must not be edited here. |
368
+ | `renderSuggestionLine` | function | Renders the line listing an open select's offered values above its text prompt — a suggestion list, because an open select admits an answer the list does not offer. |
369
+ | `renderUnavailableLine` | function | Renders the line naming the choices a field shows but refuses, written above the list the walk drives. |
370
+ | `renderNumberedList` | function | Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate with arrow keys, so each offered choice is printed with the number the reader types back. One line per choice, with no trailing newline. |
371
371
 
372
372
  ### The server constants
373
373
 
@@ -385,6 +385,7 @@ A `Shape` cell holds the constant's declared type.
385
385
  | `CLEAR_DOWN` | const | `string` | Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the whole previous view, which a `select` or `checkbox` can spread over several lines, in one write before the new view is rendered, so a redraw never leaves orphaned rows behind. |
386
386
  | `CONTROL_HINTS` | const | `Readonly<Partial<Record<FieldControl, string>>>` | Holds the format cue appended to a field's label for each control the walk reads as a line of text — the terminal has no date picker, no color well, and no file chooser, so the accepted shape is stated instead. A control with no entry needs none: `text` and `editor` accept any line, `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by format. The form's own rules still decide whether the typed value is acceptable. |
387
387
  | `FILE_HINT` | const | `string` | Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. |
388
+ | `LIST_HINT` | const | `string` | Holds the instruction a `list` field shows before its entries — one entry per line, and a blank line ends the list. |
388
389
  | `SUGGESTION_LEAD` | const | `string` | Holds the lead on the line listing an open `select`'s offered values, which a typed answer can ignore. |
389
390
  | `UNAVAILABLE_LEAD` | const | `string` | Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a reader sees why one is missing from the list it heads. |
390
391
  | `LOCKED_MARK` | const | `string` | Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. |
@@ -561,9 +562,10 @@ These invariants hold across `src/core`, `src/server`, and this guide.
561
562
  carries no `name` with the raw sequence preserved, so the driver cannot crash on a stray byte.
562
563
  `KeyEvent.name` is therefore optional and absence is `undefined`: an undecoded key has no name
563
564
  rather than an empty one, and every reducer reads it as a key it does not consume.
564
- 10. **Every control reaches a reducer.** `text`, `number`, `date`, `time`, `datetime`, `color`, and
565
- each `file` entry are read as one line of text through `fieldToText`, which appends that
566
- control's format cue to the label; `password`, `confirm`, `editor`, `select`, and `checkbox`
565
+ 10. **Every control reaches a reducer.** `text`, `number`, `date`, `time`, `datetime`, `color`, each
566
+ `list` entry, and each `file` entry are read as one line of text through `fieldToText`, which
567
+ appends that control's format cue to the label; a `list` reads one entry per line until a blank
568
+ one and keeps its declared `default` when none is typed; `password`, `confirm`, `editor`, `select`, and `checkbox`
567
569
  each drive their own reducer. An open select is a suggestion list plus a typed line, because
568
570
  `open` means the answer need not come from the list. A disabled choice is named on an
569
571
  unavailable line and never offered, since the form refuses its value at every door. A `hidden`