@orkestrel/scaffold 0.0.66 → 0.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/main.js +67 -44
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/templates/brief.md +9 -0
- package/dist/host/claude/agents/orkestrel.md +8 -8
- package/dist/host/claude/rules/names.md +15 -0
- package/dist/host/claude/rules/tests.md +33 -4
- package/dist/host/claude/rules/workspace.md +14 -2
- package/dist/host/dotfiles/prettierignore +3 -0
- package/dist/host/guides/README.md +65 -0
- package/dist/host/guides/abort.md +169 -0
- package/dist/host/guides/agent.md +1509 -0
- package/dist/host/guides/brief.md +1266 -0
- package/dist/host/guides/browser.md +2200 -0
- package/dist/host/guides/budget.md +196 -0
- package/dist/host/guides/codec.md +519 -0
- package/dist/host/guides/console.md +785 -0
- package/dist/host/guides/contract.md +1193 -0
- package/dist/host/guides/csv.md +541 -0
- package/dist/host/guides/database.md +2518 -0
- package/dist/host/guides/emitter.md +233 -0
- package/dist/host/guides/form.md +1791 -0
- package/dist/host/guides/html.md +717 -0
- package/dist/host/guides/indexeddb.md +505 -0
- package/dist/host/guides/interpret.md +1029 -0
- package/dist/host/guides/lsp.md +515 -0
- package/dist/host/guides/markdown.md +964 -0
- package/dist/host/guides/mcp.md +5554 -0
- package/dist/host/guides/middleware.md +927 -0
- package/dist/host/guides/msg.md +440 -0
- package/dist/host/guides/ndjson.md +120 -0
- package/dist/host/guides/ollama.md +380 -0
- package/dist/host/guides/pool.md +280 -0
- package/dist/host/guides/probe.md +1210 -0
- package/dist/host/guides/process.md +1620 -0
- package/dist/host/guides/program.md +1110 -0
- package/dist/host/guides/qualifier.md +854 -0
- package/dist/host/guides/queue.md +370 -0
- package/dist/host/guides/rater.md +330 -0
- package/dist/host/guides/reason.md +1122 -0
- package/dist/host/guides/relation.md +373 -0
- package/dist/host/guides/router.md +753 -0
- package/dist/host/guides/scaffold.md +192 -31
- package/dist/host/guides/sea.md +383 -0
- package/dist/host/guides/server.md +752 -0
- package/dist/host/guides/sqlite.md +330 -0
- package/dist/host/guides/sse.md +187 -0
- package/dist/host/guides/supervisor.md +4890 -0
- package/dist/host/guides/table.md +1556 -0
- package/dist/host/guides/template.md +280 -0
- package/dist/host/guides/terminal.md +1145 -0
- package/dist/host/guides/test.md +2969 -0
- package/dist/host/guides/timeout.md +252 -0
- package/dist/host/guides/tool.md +311 -0
- package/dist/host/guides/toolbox.md +1038 -0
- package/dist/host/guides/websocket.md +282 -0
- package/dist/host/guides/worker.md +615 -0
- package/dist/host/guides/workflow.md +1507 -0
- package/dist/host/guides/workspace.md +595 -0
- package/dist/host/manifest.json +1218 -10
- package/dist/host/tests/policy.test.ts +279 -2
- package/dist/host/tests/setupPolicy.ts +437 -6
- package/dist/src/core/index.cjs +44 -22
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +33 -9
- package/dist/src/core/index.d.ts +33 -9
- package/dist/src/core/index.js +43 -23
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1750 -1567
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +106 -24
- package/dist/src/server/index.d.ts +106 -24
- package/dist/src/server/index.js +1751 -1570
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# Workspace
|
|
2
|
+
|
|
3
|
+
> The virtual file workspace for the `@orkestrel` line: a path-keyed map of immutable files with
|
|
4
|
+
> an editing surface over it, a registry that holds those maps by id under one active selection,
|
|
5
|
+
> and a snapshot store seam that persists them.
|
|
6
|
+
|
|
7
|
+
Every edit — `write`, `prepend`, `append`, `replace`, `move` — mints a new `FileInterface` value
|
|
8
|
+
and puts it back under its path, so a file is a value a caller can hold and compare, never a handle
|
|
9
|
+
that changes underneath it. `Workspace` is the class behind the editing surface and
|
|
10
|
+
`WorkspaceManager` the class behind the registry, which gains lenient `open` and `save` whenever a
|
|
11
|
+
store is supplied. `WorkspaceStoreInterface` is the durability seam: `get`, `set`, and `delete` over
|
|
12
|
+
a `WorkspaceSnapshot`. Everything else in this module is the immutable data those nouns exchange,
|
|
13
|
+
plus the pure functions that derive it. Source: [`src/core`](../src/core). Published through
|
|
14
|
+
`@orkestrel/workspace`.
|
|
15
|
+
|
|
16
|
+
A workspace is not a filesystem. There is no disk, no `node:fs`, no watcher, no synchronization
|
|
17
|
+
lifecycle, and no dirty-state tracking. A path is a key, not a location: `src/main.ts` and
|
|
18
|
+
`notes.md` sit in the same flat map with no directories between them, and nothing outside the
|
|
19
|
+
process can change what the map holds. Durability is a separate seam — `snapshot()` produces a
|
|
20
|
+
plain JSON-serializable value and a store persists it. A store that one day wrote those snapshots
|
|
21
|
+
to disk would be one more implementation of that interface, not a change of identity here.
|
|
22
|
+
|
|
23
|
+
Anyone can drive it. An agent loop, a tool handler, and plain application code are all callers.
|
|
24
|
+
|
|
25
|
+
## Surface
|
|
26
|
+
|
|
27
|
+
### Contracts
|
|
28
|
+
|
|
29
|
+
The data shapes, from [`types.ts`](../src/core/types.ts). Every property is readonly, and an
|
|
30
|
+
absent optional field is absent. `WorkspaceInterface`, `WorkspaceManagerInterface`, and
|
|
31
|
+
`WorkspaceStoreInterface` are the behavioral contracts: each one's call-signature members are
|
|
32
|
+
documented under [`## Methods`](#methods), and its readonly data members stay here — `id`,
|
|
33
|
+
`emitter`, and `count` on `WorkspaceInterface`, `count` and `active` on
|
|
34
|
+
`WorkspaceManagerInterface`, and none on `WorkspaceStoreInterface`.
|
|
35
|
+
|
|
36
|
+
A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
|
|
37
|
+
|
|
38
|
+
| Name | Kind | Shape | Summary |
|
|
39
|
+
| --------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40
|
+
| `BinaryMIME` | type | `'image/png' \| 'image/jpeg' \| 'image/gif' \| 'image/webp'` | Names the MIME labels a binary `FileContent` arm supports. |
|
|
41
|
+
| `FileContent` | type | `TextContent \| BinaryContent` | Holds a file's immutable content: either text with a language tag or a base64 string with a MIME. The union carries no discriminant field, so a caller narrows it with a guard. |
|
|
42
|
+
| `TextContent` | interface | `{ text, language }` | Holds the text arm of a file's immutable content: a body and its language tag. |
|
|
43
|
+
| `BinaryContent` | interface | `{ base64, mime }` | Holds the binary arm of a file's immutable content: a base64 payload and its MIME. |
|
|
44
|
+
| `FileState` | type | `'created' \| 'modified'` | Names the edit state of an immutable file value: `created` for the first write to a path and `modified` for every later edit of that path. |
|
|
45
|
+
| `FileInput` | interface | `{ path, content, state? }` | Carries the caller-supplied values used to create an immutable file. The byte size and the line count are derived rather than supplied. |
|
|
46
|
+
| `FileInterface` | interface | `{ path, content, state, size, lines }` | Represents an immutable path-addressed file with derived byte and line counts. |
|
|
47
|
+
| `Position` | interface | `{ line, column }` | Locates a 1-based caret inside text. |
|
|
48
|
+
| `Range` | interface | `{ start, end }` | Represents a half-open text span whose start is inclusive and end is exclusive. |
|
|
49
|
+
| `ReadResult` | interface | `{ content, range }` | Carries the content and clamped span returned by a ranged read. |
|
|
50
|
+
| `SearchOptions` | interface | `{ regex?, sensitive?, limit? }` | Configures search and replacement behavior. |
|
|
51
|
+
| `SearchMatch` | interface | `{ path, line, column, length, content }` | Reports one 1-based search hit and the full line that contains it. |
|
|
52
|
+
| `ReplaceResult` | interface | `{ occurrences, files }` | Carries the tallies a replacement produced: the occurrences replaced and the files changed. |
|
|
53
|
+
| `WorkspaceEventMap` | type | `{ write, remove, move, clear }` | Names the events emitted after workspace mutations complete. |
|
|
54
|
+
| `WorkspaceOptions` | interface | `{ id?, on?, error?, seed? }` | Configures a workspace at construction. |
|
|
55
|
+
| `WorkspaceSnapshot` | interface | `{ id, files }` | Represents a workspace's stored state in JSON-serializable form. |
|
|
56
|
+
| `WorkspaceStoreInterface` | interface | `{} plus get, set, delete` | Persists workspace snapshots through an asynchronous point-access contract. |
|
|
57
|
+
| `WorkspaceSnapshotRow` | interface | `{ id, snapshot }` | Represents the database row used to persist one opaque workspace snapshot. |
|
|
58
|
+
| `WorkspaceErrorCode` | type | `'MISSING' \| 'MODALITY' \| 'PATTERN' \| 'RANGE'` | Names the machine-readable failure codes raised by the workspace edit surface. |
|
|
59
|
+
| `WorkspaceInterface` | interface | `{ id, emitter, count } plus file, files, read, has, search, replace, write, prepend, append, move, remove, clear, snapshot, destroy` | Represents a mutable path-keyed editing surface over immutable file values. |
|
|
60
|
+
| `WorkspaceManagerOptions` | interface | `{ on?, error?, store? }` | Configures a workspace registry at construction. |
|
|
61
|
+
| `WorkspaceManagerInterface` | interface | `{ count, active } plus workspace, workspaces, add, switch, open, save, remove, clear` | Represents an insertion-ordered workspace registry with an active selection and optional durability. |
|
|
62
|
+
|
|
63
|
+
### Constants
|
|
64
|
+
|
|
65
|
+
A `Shape` cell holds the constant's declared type.
|
|
66
|
+
|
|
67
|
+
| Name | Kind | Shape | Summary |
|
|
68
|
+
| --------------------- | ----- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `EXTENSION_LANGUAGES` | const | `Readonly<Record<string, string>>` | Maps file extensions to language tags for text content. The table is frozen, and an extension it does not list falls back to `text` in `inferLanguage`. |
|
|
70
|
+
|
|
71
|
+
### Errors
|
|
72
|
+
|
|
73
|
+
From [`errors.ts`](../src/core/errors.ts). A refusal is an exception; everything else this
|
|
74
|
+
package can answer, it answers with a value.
|
|
75
|
+
|
|
76
|
+
| Name | Kind | Signature | Summary |
|
|
77
|
+
| ------------------ | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `WorkspaceError` | class | `new (code, message, context?)` | Reports an invalid workspace edit or search operation, carrying a `WorkspaceErrorCode` and, when the operation had one, the context it ran under. |
|
|
79
|
+
| `isWorkspaceError` | function | `(value: unknown) => value is WorkspaceError` | Narrows a caught value to a `WorkspaceError`. |
|
|
80
|
+
|
|
81
|
+
### Helpers
|
|
82
|
+
|
|
83
|
+
The pure leaves, from [`helpers.ts`](../src/core/helpers.ts). Each one is exported and tested
|
|
84
|
+
on its own, and the classes compose them rather than hiding them.
|
|
85
|
+
|
|
86
|
+
| Name | Kind | Signature | Summary |
|
|
87
|
+
| -------------------- | -------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
88
|
+
| `inferLanguage` | function | `(path: string) => string` | Infers a language tag from the final file extension. |
|
|
89
|
+
| `isText` | function | `(content: FileContent) => content is TextContent` | Determines whether content is the text arm. |
|
|
90
|
+
| `isBinary` | function | `(content: FileContent) => content is BinaryContent` | Determines whether content is the binary arm. |
|
|
91
|
+
| `computeSize` | function | `(content: FileContent) => number` | Computes the byte size of file content. |
|
|
92
|
+
| `countLines` | function | `(content: FileContent) => number` | Counts the lines in file content. |
|
|
93
|
+
| `computeDecodedSize` | function | `(base64: string) => number` | Computes the decoded byte length of a base64 string, arithmetically rather than by decoding it. |
|
|
94
|
+
| `isValidRange` | function | `(range: Range) => boolean` | Determines whether a 1-based range is structurally valid. |
|
|
95
|
+
| `clampPosition` | function | `(text: string, position: Position) => Position` | Clamps a position to text bounds. |
|
|
96
|
+
| `clampRange` | function | `(text: string, range: Range) => Range` | Clamps both positions in a range to text bounds. |
|
|
97
|
+
| `offsetAt` | function | `(text: string, position: Position) => number` | Converts a 1-based position to a zero-based string offset. |
|
|
98
|
+
| `sliceRange` | function | `(text: string, range: Range) => string` | Slices a clamped half-open text range. |
|
|
99
|
+
| `spliceRange` | function | `(text: string, range: Range, replacement: string) => string` | Replaces a clamped half-open text range. |
|
|
100
|
+
| `rangeOf` | function | `(fromLine, fromColumn, toLine, toColumn) => Range` | Assembles a nested range from four 1-based coordinates. |
|
|
101
|
+
| `escapeRegExp` | function | `(value: string) => string` | Escapes regular-expression metacharacters for literal matching. |
|
|
102
|
+
|
|
103
|
+
### Validators
|
|
104
|
+
|
|
105
|
+
The total guards, from [`validators.ts`](../src/core/validators.ts). Each narrows an `unknown`
|
|
106
|
+
value arriving from outside the process without throwing on a hostile property access.
|
|
107
|
+
|
|
108
|
+
In a guard table a `Shape` cell holds the type the guard narrows to.
|
|
109
|
+
|
|
110
|
+
| Name | Kind | Shape | Summary |
|
|
111
|
+
| --------------------- | -------- | ------------------- | ----------------------------------------------------- |
|
|
112
|
+
| `isFile` | function | `FileInterface` | Narrows an unknown value to an immutable file record. |
|
|
113
|
+
| `isWorkspaceSnapshot` | function | `WorkspaceSnapshot` | Narrows an unknown value to a workspace snapshot. |
|
|
114
|
+
|
|
115
|
+
### Factories
|
|
116
|
+
|
|
117
|
+
From [`factories.ts`](../src/core/factories.ts) — the constructor-free way to reach every
|
|
118
|
+
class. Each returns the interface, not the class.
|
|
119
|
+
|
|
120
|
+
| Name | Kind | Signature | Summary |
|
|
121
|
+
| ------------------------------ | -------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| `createFile` | function | `(input: FileInput) => FileInterface` | Creates an immutable file with derived size and line counts. |
|
|
123
|
+
| `createTextContent` | function | `(text: string, language: string) => TextContent` | Creates the text arm of `FileContent`, returned as `TextContent` rather than as the whole union. |
|
|
124
|
+
| `createBinaryContent` | function | `(base64: string, mime: BinaryMIME) => BinaryContent` | Creates the binary arm of `FileContent`, returned as `BinaryContent` rather than as the whole union. |
|
|
125
|
+
| `createWorkspace` | function | `(options?: WorkspaceOptions) => WorkspaceInterface` | Creates a workspace with the same identity, emitter, and seed options the constructor takes. |
|
|
126
|
+
| `createMemoryWorkspaceStore` | function | `() => WorkspaceStoreInterface` | Creates an in-memory workspace snapshot store. |
|
|
127
|
+
| `createDatabaseWorkspaceStore` | function | `(driver?: DriverInterface) => WorkspaceStoreInterface` | Creates a database-backed workspace snapshot store, over an in-memory driver when the caller supplies none. |
|
|
128
|
+
| `createWorkspaceManager` | function | `(options?: WorkspaceManagerOptions) => WorkspaceManagerInterface` | Creates an empty workspace registry. |
|
|
129
|
+
|
|
130
|
+
### Classes
|
|
131
|
+
|
|
132
|
+
The implementing classes, from [`Workspace.ts`](../src/core/workspaces/Workspace.ts),
|
|
133
|
+
[`WorkspaceManager.ts`](../src/core/workspaces/WorkspaceManager.ts),
|
|
134
|
+
[`MemoryWorkspaceStore.ts`](../src/core/workspaces/stores/MemoryWorkspaceStore.ts), and
|
|
135
|
+
[`DatabaseWorkspaceStore.ts`](../src/core/workspaces/stores/DatabaseWorkspaceStore.ts) — each
|
|
136
|
+
documented in full under its own heading following this table.
|
|
137
|
+
|
|
138
|
+
| Name | Kind | Summary |
|
|
139
|
+
| ------------------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| `Workspace` | class | Implements `WorkspaceInterface` over one insertion-ordered path map the instance owns, projecting fresh arrays on every read. Whole-file edits create text files, ranged edits operate only on existing text files, and binary files remain available through construction-time hydration. Mutations emit after the file map has changed. |
|
|
141
|
+
| `WorkspaceManager` | class | Implements `WorkspaceManagerInterface` over an insertion-ordered id map and one active id the instance owns, resolving `active` through that map on every read and holding no emitter. A supplied store adds lenient snapshot `open` and `save` operations. Event defaults flow into workspaces created through the registry, while observability remains owned by each workspace. |
|
|
142
|
+
| `MemoryWorkspaceStore` | class | Holds workspace snapshots in the current process. |
|
|
143
|
+
| `DatabaseWorkspaceStore` | class | Persists workspace snapshots in a database table. Snapshots occupy one opaque column and are narrowed when read back from the storage boundary. |
|
|
144
|
+
|
|
145
|
+
### `Workspace`
|
|
146
|
+
|
|
147
|
+
The implementing class of `WorkspaceInterface`, from
|
|
148
|
+
[`Workspace.ts`](../src/core/workspaces/Workspace.ts). One insertion-ordered path map is its
|
|
149
|
+
whole state, and `files()` and `snapshot()` project fresh arrays out of it rather than exposing a
|
|
150
|
+
view. Its constructor takes one optional `WorkspaceOptions` value. The `seed` iterable seats
|
|
151
|
+
pre-built files by each value's `path`, silently and with the last duplicate path winning. That
|
|
152
|
+
seed is the only way a binary file enters a workspace, because the edit surface itself mints text
|
|
153
|
+
and nothing else. See [`## Methods`](#methods) for its public call surface.
|
|
154
|
+
|
|
155
|
+
### `WorkspaceManager`
|
|
156
|
+
|
|
157
|
+
The implementing class of `WorkspaceManagerInterface`, from
|
|
158
|
+
[`WorkspaceManager.ts`](../src/core/workspaces/WorkspaceManager.ts). It holds an
|
|
159
|
+
insertion-ordered id map plus one active id, and resolves `active` through that map on every read
|
|
160
|
+
so the pointer can never go stale. It owns no emitter: observation belongs to each workspace, and
|
|
161
|
+
the manager only forwards listener defaults into the workspaces it creates. See
|
|
162
|
+
[`## Methods`](#methods) for its public call surface.
|
|
163
|
+
|
|
164
|
+
### `MemoryWorkspaceStore`
|
|
165
|
+
|
|
166
|
+
The process-local implementation of `WorkspaceStoreInterface`, from
|
|
167
|
+
[`MemoryWorkspaceStore.ts`](../src/core/workspaces/stores/MemoryWorkspaceStore.ts). A plain map
|
|
168
|
+
behind the async contract: snapshots live as long as the process does. See
|
|
169
|
+
[`## Methods`](#methods) for the contract it satisfies.
|
|
170
|
+
|
|
171
|
+
### `DatabaseWorkspaceStore`
|
|
172
|
+
|
|
173
|
+
The durable implementation of `WorkspaceStoreInterface`, from
|
|
174
|
+
[`DatabaseWorkspaceStore.ts`](../src/core/workspaces/stores/DatabaseWorkspaceStore.ts). It
|
|
175
|
+
writes each snapshot as one opaque column of a `WorkspaceSnapshotRow` through `@orkestrel/database`
|
|
176
|
+
and narrows the column back with `isWorkspaceSnapshot` on the way out, so a row holding anything
|
|
177
|
+
else reads as absent rather than as a broken workspace. How durable it actually is belongs to the
|
|
178
|
+
driver. See [`## Methods`](#methods) for the contract it satisfies.
|
|
179
|
+
|
|
180
|
+
## Methods
|
|
181
|
+
|
|
182
|
+
The public call-signature members of each behavioral interface, one table per interface. A
|
|
183
|
+
`Summary` cell carries its member's first overload; where a member is overloaded — `read`, `has`,
|
|
184
|
+
`write`, `prepend`, `append`, `move`, and `remove` on `WorkspaceInterface`, and `remove` on
|
|
185
|
+
`WorkspaceManagerInterface` — the `Returns` cell spans the whole set and the sections that follow
|
|
186
|
+
work each form.
|
|
187
|
+
|
|
188
|
+
#### `WorkspaceInterface`
|
|
189
|
+
|
|
190
|
+
| Method | Returns | Summary |
|
|
191
|
+
| ---------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
192
|
+
| `file` | `FileInterface \| undefined` | Finds the file value stored at one path. |
|
|
193
|
+
| `files` | `readonly FileInterface[]` | Lists every file in insertion order. |
|
|
194
|
+
| `read` | text, `ReadResult`, or a record | Reads a text file whole. |
|
|
195
|
+
| `has` | `boolean` | Checks whether one path is present. |
|
|
196
|
+
| `search` | `readonly SearchMatch[]` | Scans text files for a query, skipping binary content. |
|
|
197
|
+
| `replace` | `ReplaceResult` | Rewrites every match across the text files, skipping binary content. |
|
|
198
|
+
| `write` | `void` | Writes whole text to a path, creating it when absent and retyping a binary path as text. |
|
|
199
|
+
| `prepend` | `void` | Puts text before a path's existing content, treating an absent path as empty text. |
|
|
200
|
+
| `append` | `void` | Puts text after a path's existing content, treating an absent path as empty text. |
|
|
201
|
+
| `move` | `boolean` | Re-keys one file to a new path, keeping the source's insertion slot. |
|
|
202
|
+
| `remove` | `boolean` | Drops one path, leaving an absent path untouched. |
|
|
203
|
+
| `clear` | `void` | Empties the workspace and emits one `clear`, never a burst of per-path removals. |
|
|
204
|
+
| `snapshot` | `WorkspaceSnapshot` | Projects the identity and a flat file list into a serializable value. |
|
|
205
|
+
| `destroy` | `void` | Releases the owned emitter, leaving the editing surface functional and unobserved. |
|
|
206
|
+
|
|
207
|
+
#### `WorkspaceManagerInterface`
|
|
208
|
+
|
|
209
|
+
| Method | Returns | Summary |
|
|
210
|
+
| ------------ | ------------------------------------------ | -------------------------------------------------------------------------------------------- |
|
|
211
|
+
| `workspace` | `WorkspaceInterface \| undefined` | Finds one registered workspace by id. |
|
|
212
|
+
| `workspaces` | `readonly WorkspaceInterface[]` | Lists registered workspaces in insertion order. |
|
|
213
|
+
| `add` | `WorkspaceInterface` | Creates and registers a workspace, activating it when no selection is active yet. |
|
|
214
|
+
| `switch` | `WorkspaceInterface \| undefined` | Re-points the active selection at a registered workspace. |
|
|
215
|
+
| `open` | `Promise<WorkspaceInterface \| undefined>` | Activates a registered workspace, or hydrates one from a stored snapshot on a registry miss. |
|
|
216
|
+
| `save` | `Promise<boolean>` | Persists a registered workspace's snapshot under its own id. |
|
|
217
|
+
| `remove` | `boolean` | Drops one registered workspace and destroys it. |
|
|
218
|
+
| `clear` | `void` | Empties the registry, destroying each workspace and clearing the selection. |
|
|
219
|
+
|
|
220
|
+
#### `WorkspaceStoreInterface`
|
|
221
|
+
|
|
222
|
+
| Method | Returns | Summary |
|
|
223
|
+
| -------- | ----------------------------------------- | -------------------------------------------------------- |
|
|
224
|
+
| `get` | `Promise<WorkspaceSnapshot \| undefined>` | Resolves a snapshot by workspace id. |
|
|
225
|
+
| `set` | `Promise<void>` | Inserts or replaces a snapshot under its own identifier. |
|
|
226
|
+
| `delete` | `Promise<void>` | Deletes a snapshot when present. |
|
|
227
|
+
|
|
228
|
+
## Files and content
|
|
229
|
+
|
|
230
|
+
A file is a frozen value: a `path`, its `content`, a `state`, and the derived `size` and `lines`.
|
|
231
|
+
Nothing mutates it. An edit replaces the value stored at a path, so a reference taken before an
|
|
232
|
+
edit still describes exactly what was there.
|
|
233
|
+
|
|
234
|
+
`FileContent` is a tagless union — text carries `{ text, language }`, binary carries
|
|
235
|
+
`{ base64, mime }` — and callers narrow it with a guard instead of reading a discriminant that could
|
|
236
|
+
disagree with the payload:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import {
|
|
240
|
+
computeSize,
|
|
241
|
+
countLines,
|
|
242
|
+
createBinaryContent,
|
|
243
|
+
createFile,
|
|
244
|
+
createTextContent,
|
|
245
|
+
inferLanguage,
|
|
246
|
+
isBinary,
|
|
247
|
+
isText,
|
|
248
|
+
} from '@orkestrel/workspace'
|
|
249
|
+
|
|
250
|
+
const note = createFile({
|
|
251
|
+
path: 'notes.md',
|
|
252
|
+
content: createTextContent('# Title\nBody', inferLanguage('notes.md')), // 'markdown'
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
note.size // 12 — UTF-8 bytes, through computeSize
|
|
256
|
+
note.lines // 2 — through countLines
|
|
257
|
+
note.state // 'created'
|
|
258
|
+
isText(note.content) // true
|
|
259
|
+
|
|
260
|
+
const icon = createFile({ path: 'icon.png', content: createBinaryContent('AAAA', 'image/png') })
|
|
261
|
+
isBinary(icon.content) // true
|
|
262
|
+
icon.size // 3 — decoded base64 bytes, through computeDecodedSize
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Language is inferred once, from the final path extension through `EXTENSION_LANGUAGES`, and an
|
|
266
|
+
unlisted extension resolves to `text`. Re-writing an existing text file keeps the language it
|
|
267
|
+
already had, so a caller that set one deliberately does not lose it to a rename-shaped write.
|
|
268
|
+
|
|
269
|
+
`FileState` names the first write to a path `created` and every later edit of that path
|
|
270
|
+
`modified`. Hydration preserves whichever stored state the file already carries verbatim; it does
|
|
271
|
+
not synthesize provenance. There is no dirty tracking, and `isFile` is
|
|
272
|
+
the total guard for a value arriving from outside this process.
|
|
273
|
+
|
|
274
|
+
## Editing
|
|
275
|
+
|
|
276
|
+
Editing returns its documented value when the request has meaning and throws only when a text
|
|
277
|
+
operation cannot be applied. Missing lookups and no-op moves or removals answer with values rather
|
|
278
|
+
than exceptions; `MISSING`, `MODALITY`, and `RANGE` identify the edit refusals that follow.
|
|
279
|
+
|
|
280
|
+
A write takes whole text, a clamped range, or a record batch. Prepend and append are the opposite
|
|
281
|
+
ends of the same map.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { createWorkspace, rangeOf } from '@orkestrel/workspace'
|
|
285
|
+
|
|
286
|
+
const workspace = createWorkspace({ id: 'project' })
|
|
287
|
+
|
|
288
|
+
workspace.write('src/main.ts', 'const answer = 41') // created
|
|
289
|
+
workspace.write('src/main.ts', '42', rangeOf(1, 16, 1, 18)) // modified — splices '41' → '42'
|
|
290
|
+
workspace.write({ 'README.md': '# Project', 'src/util.ts': 'export {}' }) // one file per entry
|
|
291
|
+
|
|
292
|
+
workspace.prepend('src/main.ts', '// generated\n')
|
|
293
|
+
workspace.append('src/main.ts', '\n')
|
|
294
|
+
workspace.prepend({ 'README.md': '<!-- header -->\n' })
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A whole-file write always succeeds: it creates the path or replaces what is there, and writing a
|
|
298
|
+
string over a binary path deliberately retypes it as text. Prepend and append treat an absent path
|
|
299
|
+
as empty text and create it. Aimed at binary content, though, they throw `MODALITY` — there is no
|
|
300
|
+
sensible text to concatenate onto base64 data.
|
|
301
|
+
|
|
302
|
+
A ranged write is stricter, because it addresses text that must already exist. It refuses an
|
|
303
|
+
absent path with `Cannot splice a range of a missing file: <path>` under `MISSING`, and a binary
|
|
304
|
+
path with `Cannot splice a range of a binary file: <path>` under `MODALITY`. A structurally
|
|
305
|
+
impossible range — inverted, or with a coordinate below one — throws `RANGE`. A range that is merely too
|
|
306
|
+
large is not an error: both endpoints clamp to the text's bounds, so `rangeOf(1, 2, 9, 9)` over
|
|
307
|
+
`'abc'` addresses everything from the second column onward. The pure functions behind that
|
|
308
|
+
behavior are exported and usable on their own: `isValidRange`, `clampPosition`, `clampRange`,
|
|
309
|
+
`offsetAt`, `sliceRange`, and `spliceRange`.
|
|
310
|
+
|
|
311
|
+
Ranges are half-open and 1-based. `rangeOf(1, 1, 1, 6)` covers the first five columns of line one,
|
|
312
|
+
which is the convention every editor position in this package follows.
|
|
313
|
+
|
|
314
|
+
## Reading and searching
|
|
315
|
+
|
|
316
|
+
Reads are shaped by what the caller asked for, and binary content is quietly absent from the shapes
|
|
317
|
+
that promise text:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
import { createWorkspace, escapeRegExp, rangeOf } from '@orkestrel/workspace'
|
|
321
|
+
|
|
322
|
+
const workspace = createWorkspace()
|
|
323
|
+
workspace.write({ 'a.ts': 'const x = 1\nconst y = 2', 'b.ts': 'const z = 3' })
|
|
324
|
+
|
|
325
|
+
workspace.file('a.ts') // the frozen FileInterface value, or undefined
|
|
326
|
+
workspace.files() // every file, in insertion order
|
|
327
|
+
workspace.count // 2
|
|
328
|
+
|
|
329
|
+
workspace.read('a.ts') // 'const x = 1\nconst y = 2'
|
|
330
|
+
workspace.read('a.ts', rangeOf(1, 1, 1, 6)) // { content: 'const', range: … }
|
|
331
|
+
workspace.read(['a.ts', 'missing.ts']) // { 'a.ts': … } — absent paths are omitted
|
|
332
|
+
workspace.has('a.ts') // true
|
|
333
|
+
workspace.has(['a.ts', 'b.ts']) // true — every path present
|
|
334
|
+
workspace.has(['missing.ts', 'b.ts']) // false — a batch answers true only when all are present
|
|
335
|
+
|
|
336
|
+
workspace.search('const') // three matches, a.ts before b.ts, line order within each
|
|
337
|
+
workspace.search('[a-z]\\d', { regex: true }) // pattern source instead of literal text
|
|
338
|
+
workspace.search('CONST', { sensitive: false, limit: 2 })
|
|
339
|
+
workspace.replace('const', 'let') // { occurrences: 3, files: 2 }
|
|
340
|
+
escapeRegExp('a.b') // 'a\\.b' — what a literal search does for you
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
A plain read of a binary path returns `undefined`, exactly as an absent path does; a batch read
|
|
344
|
+
omits both. A ranged read of binary content is the one that throws `MODALITY`, because the caller
|
|
345
|
+
named coordinates that cannot exist. Search and replace skip binary content entirely, so base64
|
|
346
|
+
that happens to spell a query is never a hit.
|
|
347
|
+
|
|
348
|
+
A query is literal by default — `escapeRegExp` neutralizes its metacharacters — and `regex: true`
|
|
349
|
+
passes it through as pattern source instead. `limit` caps hits for `search` and replacements for
|
|
350
|
+
`replace`, counted across files in insertion order. A pattern that will not compile throws
|
|
351
|
+
`PATTERN` rather than silently matching nothing. A zero-width pattern such as `a*` terminates: the
|
|
352
|
+
scan advances past an empty match instead of re-matching the same column.
|
|
353
|
+
|
|
354
|
+
`replace` returns `{ occurrences, files }`: how many occurrences changed and how many files they
|
|
355
|
+
were spread across. It rewrites each changed file once, so a file with four replacements emits one
|
|
356
|
+
`write` event, and a file with no match is never touched.
|
|
357
|
+
|
|
358
|
+
## Moving, removing, and snapshots
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { createWorkspace } from '@orkestrel/workspace'
|
|
362
|
+
|
|
363
|
+
const workspace = createWorkspace({ id: 'project' })
|
|
364
|
+
workspace.write({ 'old.ts': 'body', 'draft.md': 'notes' })
|
|
365
|
+
|
|
366
|
+
workspace.move('old.ts', 'src/new.ts') // true
|
|
367
|
+
workspace.move({ 'draft.md': 'docs/draft.md' }) // true — a mapping batch
|
|
368
|
+
workspace.move('ghost.ts', 'x.ts') // false — nothing to re-key
|
|
369
|
+
|
|
370
|
+
workspace.snapshot() // { id: 'project', files: [ … ] } — plain, serializable
|
|
371
|
+
|
|
372
|
+
workspace.remove('src/new.ts') // true
|
|
373
|
+
workspace.remove(['docs/draft.md', 'ghost.ts']) // false — 'ghost.ts' was never there
|
|
374
|
+
workspace.clear() // empties the workspace and emits clear
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A move re-keys a file to a new path and marks the result `modified`; the moved value carries the
|
|
378
|
+
new path, because a file's `path` is part of its value. Rebuilding the map keeps the moved value in
|
|
379
|
+
the source's insertion slot. An occupied target is removed while the source content remains in
|
|
380
|
+
that source slot, so the file count drops by one. A missing source is not a failure either:
|
|
381
|
+
`move` reports `false` and changes nothing. Moving a path to itself is the same exact no-op: it
|
|
382
|
+
returns `false`, preserves the value and order, and emits nothing.
|
|
383
|
+
|
|
384
|
+
`remove` mirrors that leniency, answering with a value rather than throwing over an absent path.
|
|
385
|
+
Each batch form — `has(paths)`, `move(mapping)`, and `remove(paths)` — applies to every entry it
|
|
386
|
+
can and reports `true` only when all of them succeeded, so one absent path turns the batch's answer
|
|
387
|
+
`false` while the present paths still move or drop. An empty batch has no entry that can fail, so
|
|
388
|
+
`has([])`, `move({})`, `remove([])`, and the registry's `remove([])` each report `true` and change
|
|
389
|
+
nothing. `clear()` owns emptying the workspace and sends one canonical `clear` event, never a burst
|
|
390
|
+
of per-path removals.
|
|
391
|
+
|
|
392
|
+
`snapshot()` is the boundary between the live surface and everything durable: an id and a flat file
|
|
393
|
+
list, holding the same frozen values the map holds. Feed those files back through a workspace's
|
|
394
|
+
construction seed and the rebuilt workspace snapshots equal.
|
|
395
|
+
|
|
396
|
+
## Events
|
|
397
|
+
|
|
398
|
+
Each workspace owns an `EmitterInterface` from `@orkestrel/emitter`, reachable as `emitter` and
|
|
399
|
+
configurable at construction through `on` and `error`. Every event fires after the map has already
|
|
400
|
+
changed, so a listener always observes the settled state.
|
|
401
|
+
|
|
402
|
+
| Event | Payload | Timing |
|
|
403
|
+
| -------- | ------------------ | -------------------------------------------------------------- |
|
|
404
|
+
| `write` | the resulting file | after a write, splice, prepend, append, or changed replacement |
|
|
405
|
+
| `remove` | the removed path | after a path that was present is dropped |
|
|
406
|
+
| `move` | `from, to` | after the map is re-keyed |
|
|
407
|
+
| `clear` | none | after `clear()` |
|
|
408
|
+
|
|
409
|
+
A listener that throws is isolated by the emitter and routed to the workspace's `error` handler:
|
|
410
|
+
the edit still lands and the call that triggered it returns normally. Nothing is emitted for a
|
|
411
|
+
no-op — removing an absent path, or replacing a query that matched nothing — and construction-time
|
|
412
|
+
seeding is silent, because seeding is hydration rather than editing.
|
|
413
|
+
|
|
414
|
+
## Lifecycle
|
|
415
|
+
|
|
416
|
+
`clear()` resets file state while leaving observation live. `destroy()` is the teardown boundary:
|
|
417
|
+
it releases the owned emitter last, is idempotent, and leaves file operations functional. Writes
|
|
418
|
+
after destruction still land, but the destroyed emitter delivers no further events.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { createWorkspace } from '@orkestrel/workspace'
|
|
422
|
+
|
|
423
|
+
const workspace = createWorkspace()
|
|
424
|
+
workspace.destroy()
|
|
425
|
+
workspace.write('silent.txt', 'still stored') // succeeds without delivering an event
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## The registry
|
|
429
|
+
|
|
430
|
+
A `WorkspaceManager` is a working set with one selection, not a global. Build one per caller, add
|
|
431
|
+
the workspaces that caller needs to reach, and let `active` say which one is current:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import { createWorkspaceManager } from '@orkestrel/workspace'
|
|
435
|
+
|
|
436
|
+
const edited: string[] = []
|
|
437
|
+
const manager = createWorkspaceManager({ on: { write: (file) => edited.push(file.path) } })
|
|
438
|
+
|
|
439
|
+
const scratch = manager.add({ id: 'scratch' }) // the first add becomes active
|
|
440
|
+
const review = manager.add({ id: 'review' }) // a later add does not steal the selection
|
|
441
|
+
|
|
442
|
+
manager.count // 2
|
|
443
|
+
manager.active === scratch // true
|
|
444
|
+
manager.workspace('review') === review // the exact registered instance, or undefined
|
|
445
|
+
manager.workspaces() // a fresh readonly array, in insertion order
|
|
446
|
+
|
|
447
|
+
manager.switch('review') // returns the workspace and re-points active
|
|
448
|
+
manager.switch('ghost') // undefined — active is left alone
|
|
449
|
+
|
|
450
|
+
manager.remove('review') // true — and active clears, because the active one went
|
|
451
|
+
manager.remove(['scratch', 'ghost']) // false — 'ghost' was never registered, but 'scratch' still goes
|
|
452
|
+
manager.clear() // empty registry, no selection
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Only the first `add` auto-activates; after that the selection moves solely by `switch`, `open`, or
|
|
456
|
+
the removal of whatever it pointed at. Adding an id that already exists replaces the registered
|
|
457
|
+
workspace, and because `active` is resolved through the map on every read, a replaced active id
|
|
458
|
+
resolves to the replacement rather than to a detached instance.
|
|
459
|
+
|
|
460
|
+
Listener defaults given to the manager flow into every workspace it creates, and per-add `on` or
|
|
461
|
+
`error` overrides them outright rather than merging. The same `WorkspaceOptions` shape reaches
|
|
462
|
+
`add`; its `seed` seats pre-built files into the new workspace silently, which is the hydration
|
|
463
|
+
seam `open` uses. `remove` and `clear` destroy each workspace as it leaves the registry. Registered
|
|
464
|
+
workspaces share nothing else: each owns its own files, its own emitter, and its own id.
|
|
465
|
+
|
|
466
|
+
## Durability
|
|
467
|
+
|
|
468
|
+
A manager without a store is complete and entirely in memory. Supplying one adds `save` and
|
|
469
|
+
`open`, and each is lenient:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
import {
|
|
473
|
+
createDatabaseWorkspaceStore,
|
|
474
|
+
createMemoryWorkspaceStore,
|
|
475
|
+
createWorkspaceManager,
|
|
476
|
+
isWorkspaceSnapshot,
|
|
477
|
+
} from '@orkestrel/workspace'
|
|
478
|
+
|
|
479
|
+
const store = createMemoryWorkspaceStore()
|
|
480
|
+
const manager = createWorkspaceManager({ store })
|
|
481
|
+
|
|
482
|
+
const project = manager.add({ id: 'project' })
|
|
483
|
+
project.write('src/main.ts', 'const answer = 42')
|
|
484
|
+
|
|
485
|
+
await manager.save('project') // true — the snapshot is now in the store
|
|
486
|
+
await manager.save('ghost') // false — unknown id, nothing written
|
|
487
|
+
|
|
488
|
+
const reader = createWorkspaceManager({ store })
|
|
489
|
+
const opened = await reader.open('project') // hydrated from the snapshot, registered, activated
|
|
490
|
+
opened?.read('src/main.ts') // 'const answer = 42'
|
|
491
|
+
await reader.open('never-saved') // undefined — a miss stays a miss
|
|
492
|
+
|
|
493
|
+
const durable = createDatabaseWorkspaceStore()
|
|
494
|
+
await durable.set(project.snapshot())
|
|
495
|
+
await durable.get('project') // the snapshot, or undefined
|
|
496
|
+
await durable.delete('project')
|
|
497
|
+
isWorkspaceSnapshot(await durable.get('project')) // false — it is gone
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
`open` consults the registry first: a registered id is activated and returned, without
|
|
501
|
+
touching the store at all. Only a miss reaches the store, and a snapshot that comes back is
|
|
502
|
+
hydrated into a new workspace through the seed, registered, and made active even when the registry
|
|
503
|
+
was not empty. A miss with no store, or a miss the store cannot satisfy, returns `undefined`.
|
|
504
|
+
`save` writes the snapshot under its own id, so saving twice upserts rather than accumulating.
|
|
505
|
+
Removing a workspace from the registry does not delete its stored snapshot; dropping durable state
|
|
506
|
+
is the store's `delete`.
|
|
507
|
+
|
|
508
|
+
`MemoryWorkspaceStore` keeps snapshots in a map for the lifetime of the process — a real store with
|
|
509
|
+
a short memory, useful wherever durability is not the point. `DatabaseWorkspaceStore` writes one
|
|
510
|
+
opaque column through `@orkestrel/database` and narrows it back with `isWorkspaceSnapshot` on read,
|
|
511
|
+
so a row that is not a snapshot reads as absent instead of propagating. Neither store clones what
|
|
512
|
+
it is given, and neither hydrates a live workspace: turning a snapshot back into a `Workspace` is
|
|
513
|
+
the manager's job.
|
|
514
|
+
|
|
515
|
+
Because the seam is `get`, `set`, and `delete` over a plain serializable value, a caller's own
|
|
516
|
+
implementation is a peer of `MemoryWorkspaceStore` and `DatabaseWorkspaceStore`. A store that wrote
|
|
517
|
+
snapshots to disk, to object storage, or to a remote service would satisfy the same contract and
|
|
518
|
+
change nothing about what a workspace is.
|
|
519
|
+
|
|
520
|
+
## Failures
|
|
521
|
+
|
|
522
|
+
A code describes every refusal, and `WorkspaceError` carries the code plus the context the
|
|
523
|
+
operation had:
|
|
524
|
+
|
|
525
|
+
| Code | Raised by |
|
|
526
|
+
| ---------- | -------------------------------------------------------------------- |
|
|
527
|
+
| `MISSING` | a ranged write aimed at a path the workspace does not hold |
|
|
528
|
+
| `MODALITY` | a text-only operation aimed at binary content |
|
|
529
|
+
| `RANGE` | a ranged write whose range is inverted or has a coordinate below one |
|
|
530
|
+
| `PATTERN` | a search or replacement whose pattern source will not compile |
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
import { createWorkspace, isWorkspaceError } from '@orkestrel/workspace'
|
|
534
|
+
|
|
535
|
+
const workspace = createWorkspace()
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
workspace.search('(', { regex: true })
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (isWorkspaceError(error)) error.code // 'PATTERN'
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Everything else answers with a value. An absent path reads as `undefined`, a fruitless `remove` or
|
|
545
|
+
`move` reports `false`, an unknown `switch` or `open` returns `undefined`, and a `save` without a
|
|
546
|
+
store returns `false`. A caller drives this package without a try block until it addresses content
|
|
547
|
+
in a way that cannot mean anything.
|
|
548
|
+
|
|
549
|
+
## Callers
|
|
550
|
+
|
|
551
|
+
The surface is deliberately narrow enough that no caller is special. `@orkestrel/agent` is one
|
|
552
|
+
such caller: an agent loop can keep a manager, treat `active` as the workspace under discussion,
|
|
553
|
+
and render its files into a prompt. That projection is the agent's product decision and lives
|
|
554
|
+
there, not here. `@orkestrel/toolbox` is another caller: a tool handler can expose the same
|
|
555
|
+
operations as callable tools. Plain code can skip both and build a workspace, edit it, and snapshot
|
|
556
|
+
it directly.
|
|
557
|
+
|
|
558
|
+
What a workspace holds is equally open. Files a program generated, documents fetched from
|
|
559
|
+
elsewhere, a scratch space that never becomes anything — the map does not care, because nothing
|
|
560
|
+
here reaches outside the process to check.
|
|
561
|
+
|
|
562
|
+
## Tests
|
|
563
|
+
|
|
564
|
+
- [`guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` bijection over values
|
|
565
|
+
and types, each interface ↔ class method bijection, and the equality gate: every `Summary` cell
|
|
566
|
+
against its declaration's description paragraph, the titled `Files and content` fence against the
|
|
567
|
+
`@example` block of that title (pinned so the titled pair cannot be retired silently), and the
|
|
568
|
+
README pitch against this guide's tagline. It also runs the flagship fences and asserts the
|
|
569
|
+
values their comments claim.
|
|
570
|
+
- [`helpers.test.ts`](../tests/src/core/helpers.test.ts) — content narrowing, sizing, line
|
|
571
|
+
counting, range validity and clamping, offsets, splicing, and escaping.
|
|
572
|
+
- [`factories.test.ts`](../tests/src/core/factories.test.ts) — derived metadata, frozen values,
|
|
573
|
+
and each factory's working instance.
|
|
574
|
+
- [`Workspace.test.ts`](../tests/src/core/workspaces/Workspace.test.ts) — the edit surface end
|
|
575
|
+
to end: state transitions, clamping, the modality matrix over a real binary file, search and
|
|
576
|
+
replace semantics, insertion order, events and listener isolation, and the construction seed.
|
|
577
|
+
- [`WorkspaceManager.test.ts`](../tests/src/core/workspaces/WorkspaceManager.test.ts) —
|
|
578
|
+
registration, the active pointer, listener defaults and overrides, and the store round trip
|
|
579
|
+
through `open` and `save`.
|
|
580
|
+
- [`MemoryWorkspaceStore.test.ts`](../tests/src/core/workspaces/stores/MemoryWorkspaceStore.test.ts)
|
|
581
|
+
and
|
|
582
|
+
[`DatabaseWorkspaceStore.test.ts`](../tests/src/core/workspaces/stores/DatabaseWorkspaceStore.test.ts)
|
|
583
|
+
— one shared store contract battery run against both implementations, plus each one's specific
|
|
584
|
+
path.
|
|
585
|
+
|
|
586
|
+
## See also
|
|
587
|
+
|
|
588
|
+
- [`README.md`](README.md) — the guides index.
|
|
589
|
+
- [`emitter.md`](emitter.md) — the dependency mirror for `@orkestrel/emitter`, whose isolation
|
|
590
|
+
guarantees back every workspace event.
|
|
591
|
+
- [`database.md`](database.md) — the dependency mirror for `@orkestrel/database`, the table behind
|
|
592
|
+
`DatabaseWorkspaceStore`.
|
|
593
|
+
- [`contract.md`](contract.md) — the dependency mirror for `@orkestrel/contract`, whose total
|
|
594
|
+
guards back the overload narrowing and the storage-boundary guards.
|
|
595
|
+
- [`AGENTS.md`](../AGENTS.md) — the repository's coding and documentation contract.
|