@orkestrel/scaffold 0.0.103 → 0.0.104
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/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
- package/dist/bin/main.js +16 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +3 -3
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
- package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
- package/dist/host/agents/transports/codex.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +54 -53
- package/dist/host/claude/agents/reviewer.md +4 -4
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/patterns.md +1 -1
- package/dist/host/claude/rules/quality.md +2 -2
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +34 -8
- package/dist/host/claude/rules/typescript.md +29 -0
- package/dist/host/claude/rules/workspace.md +32 -22
- package/dist/host/claude/rules/writing.md +34 -26
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -4
- package/dist/host/configs/policy.ts +13 -2
- package/dist/host/dotfiles/editorconfig +0 -1
- package/dist/host/dotfiles/gitattributes +0 -1
- package/dist/host/dotfiles/gitignore +0 -1
- package/dist/host/dotfiles/oxlintrc.json +24 -0
- package/dist/host/guides/agent.md +1638 -598
- package/dist/host/guides/budget.md +89 -29
- package/dist/host/guides/form.md +477 -91
- package/dist/host/guides/mcp.md +14 -2
- package/dist/host/guides/ollama.md +97 -73
- package/dist/host/guides/probe.md +1 -1
- package/dist/host/guides/scaffold.md +158 -101
- package/dist/host/guides/study.md +2635 -0
- package/dist/host/guides/table.md +225 -15
- package/dist/host/guides/terminal.md +22 -20
- package/dist/host/guides/test.md +689 -255
- package/dist/host/manifest.json +87 -63
- package/dist/host/tests/config.test.ts +136 -82
- package/dist/host/tests/policy.test.ts +39 -1
- package/dist/host/tests/setupPolicy.ts +86 -28
- package/dist/src/core/index.cjs +1387 -611
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +35 -20
- package/dist/src/core/index.d.ts +35 -20
- package/dist/src/core/index.js +1385 -612
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +12 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +12 -12
- package/dist/src/server/index.d.ts +12 -12
- package/dist/src/server/index.js +12 -12
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,1700 @@
|
|
|
1
|
+
# Bindings census
|
|
2
|
+
|
|
3
|
+
Run this census before a sweep under `.claude/rules/typescript.md` § Bindings, and before a review
|
|
4
|
+
rules that code conforms to that section or to `.claude/rules/architecture.md` § Middleware. Read
|
|
5
|
+
each item's heading as the keys of the rule sentences it covers and the rule bullet that holds them.
|
|
6
|
+
Find candidates with the item's signatures, and rule each candidate by the item's reading.
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
- **Population.** Apply every item to the TypeScript and Vue sources of the scanned checkout:
|
|
11
|
+
`src/`, `app/`, `tests/`, `configs/`, `studies/`, the skill scripts the checkout carries,
|
|
12
|
+
`vite.config.ts`, and the code a template literal emits. Skip `node_modules/`, `dist/`, `tmp/`,
|
|
13
|
+
and ambient `.d.ts` files. In a scaffold consumer, also skip every file scaffold vendors: the
|
|
14
|
+
vendored test set and the configuration leaves.
|
|
15
|
+
- **Tool.** Run each signature with ripgrep's default engine, `rg -n`, and add `-U` where the item
|
|
16
|
+
says a pattern spans lines.
|
|
17
|
+
- **Ruling.** Treat a signature hit as a candidate, and rule it by reading its source. A text search
|
|
18
|
+
reports on text (`.claude/rules/quality.md` § Instruments); an AST pass over the TypeScript parser
|
|
19
|
+
is the exact instrument for `variables.assignments`.
|
|
20
|
+
- **Report.** Record each ruled candidate as `path:line`, the item's key, and the ruling.
|
|
21
|
+
|
|
22
|
+
## Run the controls
|
|
23
|
+
|
|
24
|
+
Run an item's controls before trusting its clean result:
|
|
25
|
+
|
|
26
|
+
1. Write each fixture of the item to a scratch file under `tmp/probes/census/`, named as the
|
|
27
|
+
fixture's label gives.
|
|
28
|
+
2. Run the item's signatures over the scratch files, and record the hit lines of each file.
|
|
29
|
+
3. Require every hit fixture to yield a candidate the reading keeps, and every clear fixture to yield
|
|
30
|
+
no candidate or only candidates the reading drops.
|
|
31
|
+
4. Delete `tmp/probes/census/`.
|
|
32
|
+
|
|
33
|
+
When a hit fixture yields no kept candidate, or a clear fixture yields a kept one, repair the
|
|
34
|
+
signatures or the reading before you run the item over the checkout.
|
|
35
|
+
|
|
36
|
+
## Keys outside the census
|
|
37
|
+
|
|
38
|
+
- Run no item for these keys: `variables.declarations`, `derivation.expression`,
|
|
39
|
+
`iteration.counter`, `try.helper`, `try.log`, `running.definition`, `lists.derived`, `logs.loop`,
|
|
40
|
+
`logs.scan`, `exemptions.reach`, `percall.entity`, `percall.lifetime`, `percall.relative`,
|
|
41
|
+
`entity.home`, `entity.pointers`, `slots.route`, `slots.exclusions`, `resets.home`, `bound.read`,
|
|
42
|
+
`bound.several`, `bound.selection`, `bound.latch`, `registry.clear`, `registry.scope`,
|
|
43
|
+
`immutability.registry`, and `immutability.helper`.
|
|
44
|
+
- Read `logs.forms` through `logs.final`, `logs.last`, and `percall.concurrent`, which it widens to
|
|
45
|
+
logs a `reduce` call returns. Read `bound.unit` through `bound.field`. Read `running.position`
|
|
46
|
+
through `running.cell`, `bound.field`, and `registry.reads`.
|
|
47
|
+
|
|
48
|
+
## `variables.assignments` (**Variables**)
|
|
49
|
+
|
|
50
|
+
- **Precheck.** Run `\b(?:let|var)\s+[A-Za-z_$\[{]` first, and rule each hit in code, outside
|
|
51
|
+
comments and strings, under `variables.declarations`. Hold a parameter-only reading of this item
|
|
52
|
+
until every such declaration is ruled: after the precheck, a bare-identifier assignment can only
|
|
53
|
+
target a parameter, a `catch` binding, a declaration the precheck found, or a name TypeScript
|
|
54
|
+
refuses.
|
|
55
|
+
- **Signatures.**
|
|
56
|
+
- Assignment statements: `(?:^|[;{}]\s*)\s*[A-Za-z_$][\w$]*\s*(?:[-+*/%&|^]|\*\*|<<|>>>?|\?\?|\|\||&&)?=[^=>]`.
|
|
57
|
+
- Guarded assignments: `(?:\)|\belse|=>)\s*[A-Za-z_$][\w$]*\s*(?:[-+*/%&|^]|\*\*|<<|>>>?|\?\?|\|\||&&)?=[^=>]`.
|
|
58
|
+
- Updates: `(?:^|[^.#\w$])[A-Za-z_$][\w$]*\s*(?:\+\+|--)` and `(?:\+\+|--)\s*[A-Za-z_$][\w$]*\b`.
|
|
59
|
+
- Destructuring targets: `^\s*\[[^\]]*\]\s*=[^=]` and `^\s*\(\s*\{[^}]*\}\s*=[^=]`.
|
|
60
|
+
- Declaration-free loop heads: `for\s*(?:await\s*)?\(\s*[A-Za-z_$][\w$]*\s+(?:of|in)\b`.
|
|
61
|
+
- **Reading.** Keep a hit whose target resolves to a parameter of the enclosing function or to a
|
|
62
|
+
`catch` binding (**Variables**). Drop a parameter default on its own line, a public class field,
|
|
63
|
+
an enum member, a member update such as `++this.#count`, and a target the precheck found.
|
|
64
|
+
|
|
65
|
+
Hit fixture, `variables.assignments.hit.ts`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
function normalizeName(name: string): string {
|
|
69
|
+
name = name.trim()
|
|
70
|
+
return name
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function labelRow(label: string): string {
|
|
74
|
+
if (label === '') label = 'untitled'
|
|
75
|
+
return label
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeOverride(base: ProjectConfig, override?: ProjectConfig): ProjectConfig {
|
|
79
|
+
if (override !== undefined && 'command' in override && 'mode' in override) {
|
|
80
|
+
if (typeof override.mode !== 'string') {
|
|
81
|
+
throw new Error('The project invocation carries no string mode')
|
|
82
|
+
}
|
|
83
|
+
override = { mode: override.mode }
|
|
84
|
+
}
|
|
85
|
+
return override === undefined ? { ...base } : mergeConfig(base, override)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Clear fixture, `variables.assignments.clear.ts`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
class Transport {
|
|
93
|
+
#count = 0
|
|
94
|
+
|
|
95
|
+
async readWithRetries(
|
|
96
|
+
url: string,
|
|
97
|
+
timeout: number,
|
|
98
|
+
spent: number[],
|
|
99
|
+
accept?: string,
|
|
100
|
+
binary = false,
|
|
101
|
+
): Promise<TextReadResult | BytesReadResult> {
|
|
102
|
+
const id = ++this.#count
|
|
103
|
+
return binary ? readBytes(url, timeout, spent, id) : readText(url, timeout, spent, id, accept)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## `running.carriers` (**Running values**)
|
|
109
|
+
|
|
110
|
+
- **Signatures.**
|
|
111
|
+
- Mutating calls: `\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])*\.(?:push|unshift|splice|set|add|delete|clear|fill|sort|reverse|copyWithin)\(`.
|
|
112
|
+
- Property and index writes: `\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])+\s*(?:[-+*/%&|^]|\?\?|\|\||&&)?=[^=>]`.
|
|
113
|
+
- Property updates: `\b[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]]+\])+\s*(?:\+\+|--)`.
|
|
114
|
+
- Route a recorder to `logs.reads`: a `createRecorder` result, or any object whose `handler`
|
|
115
|
+
appends to its `calls`. Its writes run through `handler`, which no signature here matches.
|
|
116
|
+
- **Reading.**
|
|
117
|
+
- Resolve each hit's root identifier. Keep it when the root is a parameter, a `const`, or the
|
|
118
|
+
accumulator, the first parameter, of a `reduce` or `reduceRight` callback.
|
|
119
|
+
- Drop the roots `this`, `globalThis`, `process`, and host or DOM objects.
|
|
120
|
+
- Rule a kept hit banned under **Running values** when the change carries a flag, a counter, a
|
|
121
|
+
sum, a latest value, a one-slot cell, or appended text from one step or call to a later one, and
|
|
122
|
+
the collection is neither a log under **Lists and logs** nor an item of **Exemptions**. Drop a
|
|
123
|
+
helper's append to its owning operation's log, which **Immutability exceptions** admits.
|
|
124
|
+
|
|
125
|
+
Hit fixture, `running.carriers.hit.ts`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
async function readBody(
|
|
129
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
130
|
+
spent: number[],
|
|
131
|
+
allowance: number,
|
|
132
|
+
): Promise<boolean> {
|
|
133
|
+
for (;;) {
|
|
134
|
+
const chunk = await reader.read()
|
|
135
|
+
if (chunk.done) return true
|
|
136
|
+
spent.push(chunk.value.byteLength)
|
|
137
|
+
if (allowance - computeTotal(spent) < 0) {
|
|
138
|
+
await reader.cancel()
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Clear fixture, `running.carriers.clear.ts`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
interface Step {
|
|
149
|
+
readonly step: string
|
|
150
|
+
readonly exit: number | undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface Runner {
|
|
154
|
+
readonly dry: boolean
|
|
155
|
+
readonly steps: Step[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface Edit {
|
|
159
|
+
readonly start: number
|
|
160
|
+
readonly text: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function runStep(runner: Runner, step: string, execute: () => number): void {
|
|
164
|
+
if (runner.dry) {
|
|
165
|
+
runner.steps.push({ step, exit: undefined })
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
runner.steps.push({ step, exit: execute() })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describeRun(runner: Runner): string {
|
|
172
|
+
return runner.steps.map(({ step, exit }) => `${step}: ${String(exit)}`).join('\n')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function collectEdits(tokens: readonly Token[]): readonly Edit[] {
|
|
176
|
+
return tokens.reduce<readonly Edit[]>(
|
|
177
|
+
(log, token) =>
|
|
178
|
+
token.value === undefined ? log : [...log, { start: token.index, text: token.value }],
|
|
179
|
+
[],
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## `running.cell` and `running.position` (**Running values**)
|
|
185
|
+
|
|
186
|
+
- **Signatures.** `\[0\]`, `\.at\(\s*-1\s*\)`, `\[\s*[\w.$]+\.length\s*-\s*1\s*\]`,
|
|
187
|
+
`const\s*\[\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*[A-Za-z_$][\w$.]*\s*$`,
|
|
188
|
+
`const\s*\[\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*[A-Za-z_$][\w$.]*\.(?:keys|values|entries)\(\)`, and
|
|
189
|
+
`\.(?:values|keys|entries)\(\)\.next\(\)`.
|
|
190
|
+
- **Reading.** Keep a hit whose collection a variable, a parameter, or an accumulator holds, and that
|
|
191
|
+
holds at most one entry at every read (**Running values**). The signs are a reset before each add
|
|
192
|
+
(`length = 0`, `splice(0`, `clear()`), a cap, or a single write. Drop a read of a source list that
|
|
193
|
+
nothing carries, and a `#` registry read, which `registry.reads` rules.
|
|
194
|
+
|
|
195
|
+
Hit fixture, `running.cell.hit.ts`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
function readLatest(
|
|
199
|
+
readings: readonly Reading[],
|
|
200
|
+
admit: (reading: Reading) => boolean,
|
|
201
|
+
): Reading | undefined {
|
|
202
|
+
const latest: Reading[] = []
|
|
203
|
+
for (const reading of readings) {
|
|
204
|
+
if (admit(reading)) latest.splice(0, latest.length, reading)
|
|
205
|
+
}
|
|
206
|
+
return latest[0]
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Clear fixture, `running.cell.clear.ts`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
function readLast(readings: readonly Reading[]): Reading | undefined {
|
|
214
|
+
return readings.at(-1)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## `logs.final` (**Lists and logs**)
|
|
219
|
+
|
|
220
|
+
- **Signatures.**
|
|
221
|
+
- Replacement: `\.with\(`, `\.toSpliced\(`, `\.slice\(\s*0\s*,\s*-1\s*\)`, `\.splice\(`, and
|
|
222
|
+
`\[\s*[\w.$]+\.length\s*-\s*1\s*\]\s*=[^=]`.
|
|
223
|
+
- Text growth: `\$\{\s*[\w.$]+\.text\s*\}` and `\.text\s*\+`.
|
|
224
|
+
- **Reading.** Keep a hit whose list is a log, meaning each step appends its own result, whether the
|
|
225
|
+
loop pushes it or a `reduce` returns it, and whose step replaces, rewrites, or removes a record, or
|
|
226
|
+
builds a record's text from an earlier record's text (**Lists and logs**). Drop a fold's working
|
|
227
|
+
stack or frontier, which its steps pop.
|
|
228
|
+
|
|
229
|
+
Hit fixture, `logs.final.hit.ts`:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
interface Row {
|
|
233
|
+
readonly line: number
|
|
234
|
+
readonly text: string
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function stepRows(rows: readonly Row[], text: string, line: number): readonly Row[] {
|
|
238
|
+
const content = text.trim()
|
|
239
|
+
const last = rows.at(-1)
|
|
240
|
+
if (content.startsWith('- ') && last !== undefined) {
|
|
241
|
+
return rows.with(-1, { ...last, text: `${last.text} ${content}`.trimEnd() })
|
|
242
|
+
}
|
|
243
|
+
return [...rows, { line, text: content }]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readRows(lines: readonly string[]): readonly Row[] {
|
|
247
|
+
return lines.reduce<readonly Row[]>((rows, text, index) => stepRows(rows, text, index + 1), [])
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Clear fixture, `logs.final.clear.ts`:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
interface Edit {
|
|
255
|
+
readonly start: number
|
|
256
|
+
readonly text: string
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface Frame {
|
|
260
|
+
readonly indent: number
|
|
261
|
+
readonly key: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function collectEdits(tokens: readonly Token[]): readonly Edit[] {
|
|
265
|
+
return tokens.reduce<readonly Edit[]>((log, token) => {
|
|
266
|
+
if (log.at(-1)?.start === token.index) return log
|
|
267
|
+
return [...log, { start: token.index, text: token.text }]
|
|
268
|
+
}, [])
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function stepStack(stack: readonly Frame[], indent: number, key: string): readonly Frame[] {
|
|
272
|
+
const open = stack.filter((frame) => frame.indent < indent)
|
|
273
|
+
return [...open, { indent, key }]
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## `derivation.text` (**Derivation**)
|
|
278
|
+
|
|
279
|
+
- **Signatures**, each with `-U`:
|
|
280
|
+
- ``\.reduce(?:<[^>]*>)?\(\s*\(\s*[A-Za-z_$][\w$]*\s*,[^)]*\)\s*=>\s*(?:[A-Za-z_$][\w$]*\s*\+|`\$\{)``;
|
|
281
|
+
- `\.reduce(?:<[^>]*>)?\([\s\S]{0,400}?,\s*(?:''|"")\s*\)`;
|
|
282
|
+
- `\.reduce<\s*string\b`, a fold typed to carry text;
|
|
283
|
+
- `\.reduce(?:<[^>]*>)?\(\s*\(\s*[A-Za-z_$][\w$]*\s*,[^)]*\)\s*=>[\s\S]{0,400}?\b[A-Za-z_$][\w$]*\.text\s*\+`,
|
|
284
|
+
a fold whose record carries text.
|
|
285
|
+
- **Reading.** Keep a fold whose step returns its carried text with a part appended (**Derivation**):
|
|
286
|
+
`text + part`, a template that opens with the carried text, a helper that appends to it such as
|
|
287
|
+
`joinThinking(joined, part)`, or a returned record whose text field extends the carried one. Drop a
|
|
288
|
+
fold whose step replaces a range of a fixed source text.
|
|
289
|
+
|
|
290
|
+
Hit fixture, `derivation.text.hit.ts`:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
function foldBlock(lines: readonly string[]): string {
|
|
294
|
+
return lines.reduce(
|
|
295
|
+
(running, line) =>
|
|
296
|
+
line === ''
|
|
297
|
+
? { text: running.text, blanks: running.blanks + 1 }
|
|
298
|
+
: {
|
|
299
|
+
text:
|
|
300
|
+
running.text +
|
|
301
|
+
(running.text === ''
|
|
302
|
+
? ''
|
|
303
|
+
: running.blanks === 0
|
|
304
|
+
? ' '
|
|
305
|
+
: '\n'.repeat(running.blanks)) +
|
|
306
|
+
line,
|
|
307
|
+
blanks: 0,
|
|
308
|
+
},
|
|
309
|
+
{ text: '', blanks: 0 },
|
|
310
|
+
).text
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderLines(lines: readonly string[]): string {
|
|
314
|
+
return lines.reduce((text, line) => `${text}${line}\n`, '')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function joinPasses(passes: readonly Pass[]): string | undefined {
|
|
318
|
+
return passes.reduce<string | undefined>(
|
|
319
|
+
(joined, pass) => (pass.thinking === undefined ? joined : joinThinking(joined, pass.thinking)),
|
|
320
|
+
undefined,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Clear fixture, `derivation.text.clear.ts`:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
function applyEdits(source: string, edits: readonly Edit[]): string {
|
|
329
|
+
return [...edits]
|
|
330
|
+
.sort((left, right) => right.start - left.start)
|
|
331
|
+
.reduce((text, edit) => text.slice(0, edit.start) + edit.text + text.slice(edit.end), source)
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## `logs.last` and `logs.reads` (**Lists and logs**)
|
|
336
|
+
|
|
337
|
+
This item also rules the step-keyed maps `exemptions.memo` routes here.
|
|
338
|
+
|
|
339
|
+
- **Signatures.**
|
|
340
|
+
- Last-entry and length reads: `\.get\(\s*this\.#retries\s*\)`, `\.get\([^)]*length\s*-\s*1\s*\)`,
|
|
341
|
+
`\.at\(\s*-1\s*\)`, `\[\s*[\w.$]+\.length\s*-\s*1\s*\]`, `\.length\s*(?:[<>]=?|===?|!==?)\s*\d`,
|
|
342
|
+
and `toHaveLength\(`.
|
|
343
|
+
- Log, map, and recorder declarations:
|
|
344
|
+
`const\s+[A-Za-z_$][\w$]*\s*(?::[^=]+)?=\s*(?:\[\]|new Map\b|createRecorder\b)`. For each, list
|
|
345
|
+
the name's reads, following it through its aliases, the calls it is passed to, and the callers it
|
|
346
|
+
is returned to.
|
|
347
|
+
- Recorder reads: `\.count\b`, `\.calls\.length\b`, `\.calls\s*\)\s*\.toHaveLength\(`,
|
|
348
|
+
`\.calls\.at\(\s*-1\s*\)`, and `\.calls\[\s*[\w.$]+\.(?:length|count)\s*-\s*1\s*\]`. Resolve each
|
|
349
|
+
hit's root to its declaration, which can be a `createRecorder` call, a setup-module factory, or a
|
|
350
|
+
class whose `calls` the `handler` appends to.
|
|
351
|
+
- **Reading.**
|
|
352
|
+
- Keep a log, or a `Map` keyed by loop index or attempt, whose every read along those paths is its
|
|
353
|
+
last entry, its length, or `get` of its last key (**Lists and logs**). Count passing or returning
|
|
354
|
+
the log as no read. Drop a log that some read consumes whole (a spread, `map`, `filter`, `some`,
|
|
355
|
+
`join`, iteration, or a whole-list comparison), even when a step also looks back at its last
|
|
356
|
+
record.
|
|
357
|
+
- Treat a recorder's `calls` as its log and its `count` as that log's length, and follow it through
|
|
358
|
+
aliases, the calls it is passed to, and the callers it is returned to. Keep a recorder whose every
|
|
359
|
+
read is its `count`, its `calls` length, `toHaveLength` on its `calls`, or its last call. Drop a
|
|
360
|
+
recorder that some read consumes whole, such as a whole-history comparison of its `calls`, a `map`
|
|
361
|
+
or iteration over them, or a spread. Drop a `count` that belongs to an entity tally rather than a
|
|
362
|
+
recorder.
|
|
363
|
+
|
|
364
|
+
Hit fixture, `logs.last.hit.ts`:
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
class Reader {
|
|
368
|
+
readonly #retries: number
|
|
369
|
+
|
|
370
|
+
constructor(retries: number) {
|
|
371
|
+
this.#retries = retries
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async read(url: string): Promise<string> {
|
|
375
|
+
const notes = new Map<number, string>()
|
|
376
|
+
for (const round of Array.from({ length: this.#retries + 1 }).keys()) {
|
|
377
|
+
const outcome = await fetchText(url)
|
|
378
|
+
if (outcome.ok) return outcome.content
|
|
379
|
+
notes.set(round, outcome.note)
|
|
380
|
+
}
|
|
381
|
+
throw new Error(`The read at ${url} produced no answer: ${notes.get(this.#retries) ?? ''}`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function createJudge(asked: string[], answer: (count: number) => Promise<Verdict>): Judge {
|
|
386
|
+
return {
|
|
387
|
+
ask: (request) => {
|
|
388
|
+
asked.push(...Object.keys(request.questions))
|
|
389
|
+
return answer(asked.length)
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
it('asks the following question after the judge aborts', async () => {
|
|
395
|
+
const asked: string[] = []
|
|
396
|
+
await classify(createJudge(asked, () => Promise.reject(new Error('The judge aborted'))))
|
|
397
|
+
expect(asked).toHaveLength(2)
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Clear fixture, `logs.last.clear.ts`:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
function collectEdits(tokens: readonly Token[]): readonly Edit[] {
|
|
405
|
+
return tokens.reduce<readonly Edit[]>((log, token) => {
|
|
406
|
+
if (log.at(-1)?.start === token.index) return log
|
|
407
|
+
return [...log, { name: token.name, start: token.index }]
|
|
408
|
+
}, [])
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function declares(tokens: readonly Token[], name: string): boolean {
|
|
412
|
+
return collectEdits(tokens).some((edit) => edit.name === name)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function establish(missing: readonly string[], located: Anchor): Established {
|
|
416
|
+
const created: Anchor[] = []
|
|
417
|
+
for (const segment of [...missing].reverse()) {
|
|
418
|
+
const anchor = created.at(-1) ?? located
|
|
419
|
+
if (!matchesAnchor(anchor)) {
|
|
420
|
+
throw new Error(`The directory at ${anchor.path} changed while writing.`)
|
|
421
|
+
}
|
|
422
|
+
mkdirSync(segment)
|
|
423
|
+
created.push(readAnchor(segment))
|
|
424
|
+
}
|
|
425
|
+
return { anchor: created.at(-1) ?? located, created }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
it('establishes a nested directory segment by segment', () => {
|
|
429
|
+
const result = establish(['/workspace/project/.claude', '/workspace/project'], ROOT_ANCHOR)
|
|
430
|
+
expect(result.created.map((anchor) => anchor.path)).toEqual([
|
|
431
|
+
'/workspace/project',
|
|
432
|
+
'/workspace/project/.claude',
|
|
433
|
+
])
|
|
434
|
+
})
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Hit fixture, `logs.reads.hit.ts`:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
it('emits a change for each write', () => {
|
|
441
|
+
const seen = createRecorder<[string]>()
|
|
442
|
+
const emitter = createEmitter()
|
|
443
|
+
emitter.on('change', seen.handler)
|
|
444
|
+
emitter.emit('change', 'first')
|
|
445
|
+
emitter.emit('change', 'second')
|
|
446
|
+
expect(seen.count).toBe(2)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
function expectOneSave(store: DraftStore): void {
|
|
450
|
+
const saved = createRecorder<[string]>()
|
|
451
|
+
store.on('save', saved.handler)
|
|
452
|
+
store.save('draft')
|
|
453
|
+
expect(saved.calls).toHaveLength(1)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
it('saves a draft once', () => {
|
|
457
|
+
expectOneSave(createDraftStore())
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Clear fixture, `logs.reads.clear.ts`:
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
it('emits each write in order', () => {
|
|
465
|
+
const seen = createRecorder<[string]>()
|
|
466
|
+
const emitter = createEmitter()
|
|
467
|
+
emitter.on('change', seen.handler)
|
|
468
|
+
emitter.emit('change', 'first')
|
|
469
|
+
emitter.emit('change', 'second')
|
|
470
|
+
expect(seen.count).toBe(2)
|
|
471
|
+
expect(seen.calls).toStrictEqual([['first'], ['second']])
|
|
472
|
+
})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## `percall.concurrent` and `logs.writers` (**Per-call state**, **Lists and logs**)
|
|
476
|
+
|
|
477
|
+
- **Signatures.**
|
|
478
|
+
- Promise combinators: `Promise\.(?:all|allSettled|any|race)\(`. Inside the `async` callbacks
|
|
479
|
+
passed to one, look for `\.(?:push|unshift|set|add)\(` on a name the callback does not declare.
|
|
480
|
+
- Worker pools: `Array\.from\(\s*\{\s*length:`, with each collection parameter traced from the
|
|
481
|
+
pool's caller into the workers.
|
|
482
|
+
- Several event sources: `\.on\(\s*['"](?:data|message|chunk|error)['"]`, where more than one
|
|
483
|
+
source pushes into one list.
|
|
484
|
+
- **Reading.** Keep a list that more than one concurrently pending async path of one call appends
|
|
485
|
+
to, or that the callbacks of more than one event source append to (**Per-call state**,
|
|
486
|
+
**Lists and logs**). Drop a list one loop owns, one worker's own log, and the callbacks of one
|
|
487
|
+
event source in emission order.
|
|
488
|
+
|
|
489
|
+
Hit fixture, `percall.concurrent.hit.ts`:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
class Upstream {
|
|
493
|
+
readonly #concurrency: number
|
|
494
|
+
|
|
495
|
+
constructor(concurrency: number) {
|
|
496
|
+
this.#concurrency = concurrency
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async lookup(names: readonly string[]): Promise<readonly Release[]> {
|
|
500
|
+
const spent: number[] = []
|
|
501
|
+
return this.#gather(names, (name) => this.#release(name, spent))
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async #release(name: string, spent: number[]): Promise<Release> {
|
|
505
|
+
const body = await fetchBytes(name)
|
|
506
|
+
spent.push(body.byteLength)
|
|
507
|
+
return { name, bytes: body.byteLength }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async #gather<T, R>(items: readonly T[], run: (item: T) => Promise<R>): Promise<readonly R[]> {
|
|
511
|
+
const entries = items.entries()
|
|
512
|
+
const workers = Array.from({ length: Math.min(this.#concurrency, items.length) }, () =>
|
|
513
|
+
this.#drain(entries, run),
|
|
514
|
+
)
|
|
515
|
+
const settled = await Promise.allSettled(workers)
|
|
516
|
+
return settled.flatMap((worker) => (worker.status === 'fulfilled' ? worker.value : []))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async #drain<T, R>(
|
|
520
|
+
entries: IterableIterator<[number, T]>,
|
|
521
|
+
run: (item: T) => Promise<R>,
|
|
522
|
+
): Promise<readonly R[]> {
|
|
523
|
+
const produced: R[] = []
|
|
524
|
+
for (const [, item] of entries) produced.push(await run(item))
|
|
525
|
+
return produced
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Clear fixture, `percall.concurrent.clear.ts`:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
class Upstream {
|
|
534
|
+
readonly #concurrency: number
|
|
535
|
+
readonly #retries: number
|
|
536
|
+
|
|
537
|
+
constructor(concurrency: number, retries: number) {
|
|
538
|
+
this.#concurrency = concurrency
|
|
539
|
+
this.#retries = retries
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async lookup(names: readonly string[]): Promise<readonly Release[]> {
|
|
543
|
+
return this.#gather(names, (name) => this.#release(name))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async #release(name: string): Promise<Release> {
|
|
547
|
+
for (const round of Array.from({ length: this.#retries + 1 }).keys()) {
|
|
548
|
+
const outcome = await fetchBytes(name, round)
|
|
549
|
+
if (outcome.ok) return { name, bytes: outcome.body.byteLength }
|
|
550
|
+
}
|
|
551
|
+
return { name, bytes: 0 }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async #gather<T, R>(items: readonly T[], run: (item: T) => Promise<R>): Promise<readonly R[]> {
|
|
555
|
+
const entries = items.entries()
|
|
556
|
+
const workers = Array.from({ length: Math.min(this.#concurrency, items.length) }, () =>
|
|
557
|
+
this.#drain(entries, run),
|
|
558
|
+
)
|
|
559
|
+
const settled = await Promise.allSettled(workers)
|
|
560
|
+
return settled.flatMap((worker) => (worker.status === 'fulfilled' ? worker.value : []))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async #drain<T, R>(
|
|
564
|
+
entries: IterableIterator<[number, T]>,
|
|
565
|
+
run: (item: T) => Promise<R>,
|
|
566
|
+
): Promise<readonly R[]> {
|
|
567
|
+
const produced: R[] = []
|
|
568
|
+
for (const [, item] of entries) produced.push(await run(item))
|
|
569
|
+
return produced
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## `exemptions.traversal`, `exemptions.step`, and `exemptions.cursor` (**Exemptions**)
|
|
575
|
+
|
|
576
|
+
- **Signatures.**
|
|
577
|
+
`for\s*\(\s*const\s+(?:\[[^\]]*\]|[A-Za-z_$][\w$]*)\s+of\s+[A-Za-z_$][\w$]*(?:\.entries\(\)|\.keys\(\)|\.values\(\))?\s*\)`
|
|
578
|
+
and `while\s*\(\s*[A-Za-z_$][\w$]*\.length\b`. Keep the loops whose body adds to the collection the
|
|
579
|
+
loop iterates or tests, through `.push(`, `.unshift(`, `.set(`, or `.add(`.
|
|
580
|
+
- **Reading.** Count as one step a complete iteration that removes or visits an entry of the
|
|
581
|
+
worklist, its nested loops included, and count the entries each control path through that step can
|
|
582
|
+
add (**Exemptions**). Rule a cursor when no path adds more than one entry and none spreads a list
|
|
583
|
+
(`push(...`). Drop a loop whose step can add several entries.
|
|
584
|
+
|
|
585
|
+
Hit fixture, `exemptions.traversal.hit.ts`:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
function resolveRealPath(path: string): string | undefined {
|
|
589
|
+
const trail = [resolve(path)]
|
|
590
|
+
const pending: string[] = []
|
|
591
|
+
for (const [depth, current] of trail.entries()) {
|
|
592
|
+
if (depth > MAX_PATH_DEPTH) break
|
|
593
|
+
const real = attempt(() => realpathSync(current))
|
|
594
|
+
if (real.success) {
|
|
595
|
+
return pending.reduce((physical, segment) => join(physical, segment), real.value)
|
|
596
|
+
}
|
|
597
|
+
const status = attempt(() => lstatSync(current))
|
|
598
|
+
if (status.success) {
|
|
599
|
+
if (!status.value.isSymbolicLink()) return undefined
|
|
600
|
+
trail.push(resolve(dirname(current), readlinkSync(current)))
|
|
601
|
+
continue
|
|
602
|
+
}
|
|
603
|
+
const parent = dirname(current)
|
|
604
|
+
if (parent === current) return undefined
|
|
605
|
+
pending.unshift(relative(parent, current))
|
|
606
|
+
trail.push(parent)
|
|
607
|
+
}
|
|
608
|
+
return undefined
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function renderCauseChain(error: unknown): string {
|
|
612
|
+
const chain: unknown[] = error === undefined ? [] : [error]
|
|
613
|
+
while (chain.length < 4) {
|
|
614
|
+
const last = chain.at(-1)
|
|
615
|
+
const cause = isError(last) ? last.cause : undefined
|
|
616
|
+
if (cause === undefined) break
|
|
617
|
+
chain.push(cause)
|
|
618
|
+
}
|
|
619
|
+
return chain
|
|
620
|
+
.map((one) => (isError(one) ? `${one.name}: ${one.message}` : String(one)))
|
|
621
|
+
.join(' <- ')
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Clear fixture, `exemptions.traversal.clear.ts`:
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
function listFiles(root: string): readonly string[] {
|
|
629
|
+
const pending = [{ full: root, path: '' }]
|
|
630
|
+
const entries: Array<{ readonly path: string; readonly file: boolean }> = []
|
|
631
|
+
while (pending.length > 0) {
|
|
632
|
+
const current = pending.pop()
|
|
633
|
+
if (current === undefined) break
|
|
634
|
+
const handle = opendirSync(current.full)
|
|
635
|
+
for (;;) {
|
|
636
|
+
const entry = handle.readSync()
|
|
637
|
+
if (entry === null) break
|
|
638
|
+
const path = current.path === '' ? entry.name : `${current.path}/${entry.name}`
|
|
639
|
+
const file = !entry.isDirectory() || entry.isSymbolicLink()
|
|
640
|
+
entries.push({ path, file })
|
|
641
|
+
if (!file) pending.push({ full: join(current.full, entry.name), path })
|
|
642
|
+
}
|
|
643
|
+
handle.closeSync()
|
|
644
|
+
}
|
|
645
|
+
return entries.filter(({ file }) => file).map(({ path }) => path)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function scanAmendments(
|
|
649
|
+
amendments: ReadonlyMap<string, readonly string[]>,
|
|
650
|
+
start: string,
|
|
651
|
+
admit: (id: string) => boolean,
|
|
652
|
+
): readonly string[] {
|
|
653
|
+
const queue = [start]
|
|
654
|
+
const seen = new Set<string>()
|
|
655
|
+
const admitted: string[] = []
|
|
656
|
+
for (const id of queue) {
|
|
657
|
+
if (seen.has(id)) continue
|
|
658
|
+
seen.add(id)
|
|
659
|
+
if (!admit(id)) continue
|
|
660
|
+
admitted.push(id)
|
|
661
|
+
queue.push(...(amendments.get(id) ?? []))
|
|
662
|
+
}
|
|
663
|
+
return admitted
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function validateRuntime(instrument: string, roots: readonly string[]): void {
|
|
667
|
+
const pending = [instrument]
|
|
668
|
+
const visited = new Set<string>()
|
|
669
|
+
while (pending.length > 0) {
|
|
670
|
+
const root = pending.pop()
|
|
671
|
+
if (root === undefined || visited.has(root)) continue
|
|
672
|
+
visited.add(root)
|
|
673
|
+
if (!roots.includes(root)) throw new Error(`Profile roots omit a runtime dependency: ${root}`)
|
|
674
|
+
const manifest = join(root, 'package.json')
|
|
675
|
+
const value: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
676
|
+
if (!isRecord(value) || !isRecord(value.dependencies)) continue
|
|
677
|
+
const resolver = createRequire(manifest)
|
|
678
|
+
for (const dependency of Object.keys(value.dependencies))
|
|
679
|
+
pending.push(dirname(realpathSync(resolver.resolve(`${dependency}/package.json`))))
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function capturePackages(selections: ReadonlyMap<string, string>): readonly InstallPackage[] {
|
|
684
|
+
const sources = new Map(selections)
|
|
685
|
+
const packages: InstallPackage[] = []
|
|
686
|
+
for (const [name, source] of sources) {
|
|
687
|
+
const manifest = readManifest(source)
|
|
688
|
+
packages.push(Object.freeze({ name, source }))
|
|
689
|
+
const require = createRequire(join(source, 'package.json'))
|
|
690
|
+
for (const dependency of Object.keys(manifest.dependencies ?? {}))
|
|
691
|
+
if (!sources.has(dependency))
|
|
692
|
+
sources.set(dependency, dirname(require.resolve(`${dependency}/package.json`)))
|
|
693
|
+
}
|
|
694
|
+
return Object.freeze(packages)
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## `exemptions.memo` and `exemptions.position` (**Exemptions**)
|
|
699
|
+
|
|
700
|
+
- **Signatures.** Every `new Map\b` that a variable or a `#` field holds, and every parameter typed
|
|
701
|
+
as a map, `[A-Za-z_$][\w$]*\??\s*:\s*Map<`. Then the map's `\.set\(` and `\.delete\(` sites, and
|
|
702
|
+
`\.get\([^)]*\)\s*[!?]?\.(?:push|set|add|delete|splice|unshift)\(`.
|
|
703
|
+
- **Reading.** Keep a map treated as a memo that does any of these (**Exemptions**):
|
|
704
|
+
- writes a key twice, through a `set` path that no `has` or `get` miss for that key dominates;
|
|
705
|
+
- deletes a key;
|
|
706
|
+
- changes a stored value;
|
|
707
|
+
- caches no computation's result under its input or identity key for reuse within one owning
|
|
708
|
+
operation, in which case **Lists and logs** rules it.
|
|
709
|
+
|
|
710
|
+
Route a map keyed by loop step or attempt to `logs.last`. Drop a map that meets the memo item, and
|
|
711
|
+
a map that **Lists and logs** or **Registries** governs.
|
|
712
|
+
|
|
713
|
+
Hit fixture, `exemptions.memo.hit.ts`:
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
function readLines(paths: readonly string[], cache: Map<string, string[]>): readonly string[] {
|
|
717
|
+
return paths.flatMap((path) => {
|
|
718
|
+
const cached = cache.get(path)
|
|
719
|
+
if (cached !== undefined) return cached
|
|
720
|
+
const lines = readFileSync(path, 'utf8').split('\n')
|
|
721
|
+
cache.set(path, lines)
|
|
722
|
+
return lines
|
|
723
|
+
})
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function appendLine(cache: Map<string, string[]>, path: string, line: string): void {
|
|
727
|
+
cache.get(path)?.push(line)
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Clear fixture, `exemptions.memo.clear.ts`:
|
|
732
|
+
|
|
733
|
+
```ts
|
|
734
|
+
function placeMember(
|
|
735
|
+
input: ProjectionInput,
|
|
736
|
+
id: string,
|
|
737
|
+
seen: ReadonlySet<string>,
|
|
738
|
+
settled?: Map<string, ReadonlySet<string>>,
|
|
739
|
+
): ReadonlySet<string> {
|
|
740
|
+
const known = settled?.get(id)
|
|
741
|
+
if (known !== undefined) return known
|
|
742
|
+
const sides = (input.amending.get(id) ?? []).filter((side) => side !== id && !seen.has(side))
|
|
743
|
+
const keys = new Set(
|
|
744
|
+
sides.flatMap((side) => [...placeMember(input, side, new Set([...seen, id]), settled)]),
|
|
745
|
+
)
|
|
746
|
+
if (settled !== undefined && sides.every((side) => settled.has(side))) settled.set(id, keys)
|
|
747
|
+
return keys
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function listCollected(
|
|
751
|
+
args: readonly string[],
|
|
752
|
+
cache: Map<string, readonly Collected[]>,
|
|
753
|
+
): readonly Collected[] | undefined {
|
|
754
|
+
const key = JSON.stringify(args)
|
|
755
|
+
const cached = cache.get(key)
|
|
756
|
+
if (cached !== undefined) return cached
|
|
757
|
+
const collected = runListing(args)
|
|
758
|
+
if (collected === undefined) return undefined
|
|
759
|
+
cache.set(key, collected)
|
|
760
|
+
return collected
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function mapGateFiles(gates: ReadonlyMap<string, readonly string[]>): ReadonlyMap<string, string> {
|
|
764
|
+
const files = new Map<string, string>()
|
|
765
|
+
for (const [gate, command] of gates) {
|
|
766
|
+
for (const argument of command) {
|
|
767
|
+
if (/^tests\/[A-Za-z0-9_./-]+\.test\.ts$/u.test(argument) && !files.has(argument))
|
|
768
|
+
files.set(argument, gate)
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return files
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## `exemptions.branches` and `exemptions.platform` (**Exemptions**)
|
|
776
|
+
|
|
777
|
+
- **Signatures.** Per function body, list `\b[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*\s*=[^=]`,
|
|
778
|
+
`\b[A-Za-z_$][\w$]*\[['"][^'"]+['"]\]\s*=[^=]`, and `\.set\(\s*['"][^'"]+['"]`. Then group the
|
|
779
|
+
hits by object and key.
|
|
780
|
+
- **Reading.** Keep a key that straight-line branches write twice, or that one fixture writes twice
|
|
781
|
+
(**Exemptions**). Drop a write a platform API requires, such as a `Headers` method or a DOM
|
|
782
|
+
property.
|
|
783
|
+
|
|
784
|
+
Hit fixture, `exemptions.branches.hit.ts`:
|
|
785
|
+
|
|
786
|
+
```ts
|
|
787
|
+
function buildOptions(strict: boolean, legacy: boolean): BuildOptions {
|
|
788
|
+
const options: { mode?: string } = {}
|
|
789
|
+
if (strict) options.mode = 'strict'
|
|
790
|
+
if (legacy) options.mode = 'legacy'
|
|
791
|
+
return options
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Clear fixture, `exemptions.branches.clear.ts`:
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
function collectQuestions(target: string, writing: boolean): readonly Question[] {
|
|
799
|
+
const questions: Question[] = []
|
|
800
|
+
if (hasVueComponents(target)) {
|
|
801
|
+
questions.push({
|
|
802
|
+
field: 'extensions',
|
|
803
|
+
message:
|
|
804
|
+
'Move Vue components from app/browser to app/vue before regenerating this workspace.',
|
|
805
|
+
blocking: writing,
|
|
806
|
+
})
|
|
807
|
+
}
|
|
808
|
+
if (!writing) {
|
|
809
|
+
const script = readScriptQuestion(target)
|
|
810
|
+
if (script !== undefined) questions.push(script)
|
|
811
|
+
}
|
|
812
|
+
const project = readProjectQuestion(target, writing)
|
|
813
|
+
if (project !== undefined) questions.push(project)
|
|
814
|
+
return questions
|
|
815
|
+
}
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
## `percall.builtins` (**Per-call state**)
|
|
819
|
+
|
|
820
|
+
- **Signatures.** In an `async` function, or one that registers callbacks, find every local
|
|
821
|
+
`const\s+[A-Za-z_$][\w$]*\s*(?::[^=]+)?=\s*(?:\{|\[|new (?:Map|Set)\b)`. Keep the ones whose name a
|
|
822
|
+
`running.carriers` write signature hits inside a callback (`=>`) or after an `await`.
|
|
823
|
+
- **Reading.** Keep a plain array, record, `Map`, or `Set` that the awaits or callbacks of one call
|
|
824
|
+
change to carry state other than a log (**Per-call state**). Drop a log, an **Exemptions** item,
|
|
825
|
+
an iterator, and an `AbortController` instance.
|
|
826
|
+
|
|
827
|
+
Hit fixture, `percall.builtins.hit.ts`:
|
|
828
|
+
|
|
829
|
+
```ts
|
|
830
|
+
async function readAll(urls: readonly string[]): Promise<readonly string[]> {
|
|
831
|
+
const budget = { remaining: BUDGET_BYTES }
|
|
832
|
+
return Promise.all(
|
|
833
|
+
urls.map(async (url) => {
|
|
834
|
+
const body = await fetchText(url)
|
|
835
|
+
budget.remaining -= body.length
|
|
836
|
+
if (budget.remaining < 0) throw new Error(`The read of ${url} spent the byte allowance`)
|
|
837
|
+
return body
|
|
838
|
+
}),
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
Clear fixture, `percall.builtins.clear.ts`:
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
class Pool {
|
|
847
|
+
readonly #concurrency: number
|
|
848
|
+
|
|
849
|
+
constructor(concurrency: number) {
|
|
850
|
+
this.#concurrency = concurrency
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async gather<T, R>(items: readonly T[], run: (item: T) => Promise<R>): Promise<readonly R[]> {
|
|
854
|
+
const entries = items.entries()
|
|
855
|
+
const failed = new AbortController()
|
|
856
|
+
const workers = Array.from({ length: Math.min(this.#concurrency, items.length) }, () =>
|
|
857
|
+
this.#drain(entries, run, failed),
|
|
858
|
+
)
|
|
859
|
+
const settled = await Promise.allSettled(workers)
|
|
860
|
+
if (failed.signal.aborted) throw failed.signal.reason
|
|
861
|
+
return settled.flatMap((worker) => (worker.status === 'fulfilled' ? worker.value : []))
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async #drain<T, R>(
|
|
865
|
+
entries: IterableIterator<[number, T]>,
|
|
866
|
+
run: (item: T) => Promise<R>,
|
|
867
|
+
failed: AbortController,
|
|
868
|
+
): Promise<readonly R[]> {
|
|
869
|
+
const produced: R[] = []
|
|
870
|
+
for (const [, item] of entries) {
|
|
871
|
+
if (failed.signal.aborted) break
|
|
872
|
+
try {
|
|
873
|
+
produced.push(await run(item))
|
|
874
|
+
} catch (error) {
|
|
875
|
+
failed.abort(error)
|
|
876
|
+
break
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return produced
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
## `entity.closure` (**Entity state**)
|
|
885
|
+
|
|
886
|
+
- **Signatures**, with `-U`. A function body that declares
|
|
887
|
+
`const\s+[A-Za-z_$][\w$]*\s*(?::[^=]+)?=\s*(?:new (?:Map|Set|WeakMap|WeakSet|Array)\b|\[\]|\{\})`.
|
|
888
|
+
The body then returns a function (`return\s+(?:async\s+)?(?:function\b|\()`) or registers one
|
|
889
|
+
(`\.(?:on|once|addEventListener|use)\(`). Inside that function, a `running.carriers` write
|
|
890
|
+
signature hits the declared name. Include `describe(` bodies whose collections `it(` callbacks
|
|
891
|
+
change.
|
|
892
|
+
- **Reading.** Keep an array, record, `Map`, or `Set` that the returned or registered function
|
|
893
|
+
changes when it runs after the creating call returns (**Entity state**). Drop a collection that
|
|
894
|
+
function only reads, an entity instance, and a composable's reactive state.
|
|
895
|
+
|
|
896
|
+
Hit fixture, `entity.closure.hit.ts`:
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
function createLimiter(options: LimiterOptions): MiddlewareHandler {
|
|
900
|
+
const capacity = options.capacity ?? DEFAULT_LIMITER_CAPACITY
|
|
901
|
+
const clock = options.clock ?? Date.now
|
|
902
|
+
const buckets = new Map<string, { budget: BudgetInterface<number>; resetAt: number }>()
|
|
903
|
+
return async (_request, context, next) => {
|
|
904
|
+
const key = resolveKey(context.state)
|
|
905
|
+
const now = clock()
|
|
906
|
+
const found = buckets.get(key)
|
|
907
|
+
if (found === undefined && buckets.size >= capacity) {
|
|
908
|
+
const oldest = buckets.keys().next().value
|
|
909
|
+
if (oldest !== undefined) buckets.delete(oldest)
|
|
910
|
+
}
|
|
911
|
+
const bucket = found ?? {
|
|
912
|
+
budget: createBudget<number>({ max: options.max, consumer: Number }),
|
|
913
|
+
resetAt: now + options.window,
|
|
914
|
+
}
|
|
915
|
+
buckets.delete(key)
|
|
916
|
+
buckets.set(key, bucket)
|
|
917
|
+
if (bucket.budget.exhausted) return new Response(options.message, { status: 429 })
|
|
918
|
+
bucket.budget.consume(1)
|
|
919
|
+
return next()
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Clear fixture, `entity.closure.clear.ts`:
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
function only<TState>(
|
|
928
|
+
paths: string | readonly string[],
|
|
929
|
+
handler: MiddlewareHandler<TState>,
|
|
930
|
+
): MiddlewareHandler<TState> {
|
|
931
|
+
const matches = new Set(isString(paths) ? [paths] : paths)
|
|
932
|
+
return async (request, context, next) => {
|
|
933
|
+
if (matches.has(context.url.pathname)) return handler(request, context, next)
|
|
934
|
+
return next()
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
## `middleware.fixed` and `middleware.changing` (`.claude/rules/architecture.md` § Middleware)
|
|
940
|
+
|
|
941
|
+
- **Signatures.** Apply the `entity.closure` signatures to every `middlewares.ts` file and to every
|
|
942
|
+
function that returns a middleware.
|
|
943
|
+
- **Reading.** Keep closure state that a middleware changes across requests
|
|
944
|
+
(`.claude/rules/architecture.md` § Middleware). Drop a value the factory derives and never
|
|
945
|
+
changes, and a per-request local, which `.claude/rules/typescript.md` § Bindings rules.
|
|
946
|
+
|
|
947
|
+
Hit fixture, `middleware.fixed.hit.ts`:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
function createSession(options?: SessionMiddlewareOptions): MiddlewareHandler {
|
|
951
|
+
const path = options?.path ?? DEFAULT_SESSION_PATH
|
|
952
|
+
const ttl = options?.ttl
|
|
953
|
+
const clock = options?.clock ?? Date.now
|
|
954
|
+
const store = new Map<string, SessionEntry>()
|
|
955
|
+
return async (request, context, next) => {
|
|
956
|
+
if (context.url.pathname !== path) return next()
|
|
957
|
+
if (ttl !== undefined) {
|
|
958
|
+
const cutoff = clock() - ttl
|
|
959
|
+
for (const [id, entry] of store) {
|
|
960
|
+
if (entry.touched <= cutoff) store.delete(id)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (context.method === 'DELETE') {
|
|
964
|
+
const id = readSessionHeader(request)
|
|
965
|
+
if (id === undefined || !store.delete(id)) return rejectUnknownSession()
|
|
966
|
+
return new Response(null, { status: 204 })
|
|
967
|
+
}
|
|
968
|
+
const session = readSessionHeader(request) ?? crypto.randomUUID()
|
|
969
|
+
store.set(session, { touched: clock() })
|
|
970
|
+
return next()
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Clear fixture, `middleware.fixed.clear.ts`:
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
function only<TState>(
|
|
979
|
+
paths: string | readonly string[],
|
|
980
|
+
handler: MiddlewareHandler<TState>,
|
|
981
|
+
): MiddlewareHandler<TState> {
|
|
982
|
+
const matches = new Set(isString(paths) ? [paths] : paths)
|
|
983
|
+
return async (request, context, next) => {
|
|
984
|
+
if (matches.has(context.url.pathname)) return handler(request, context, next)
|
|
985
|
+
return next()
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
## `slots.ban` (**Call slots**)
|
|
991
|
+
|
|
992
|
+
- **Signatures.** `this\.#[A-Za-z_$][\w$]*\s*=\s*undefined\b`, plus every
|
|
993
|
+
`this\.#[A-Za-z_$][\w$]*\s*=\s*` in the opening statements of a method. Then group each field's
|
|
994
|
+
assignment sites by method, and list the settle callbacks (`.then(`, `.finally(`) each method
|
|
995
|
+
registers.
|
|
996
|
+
- **Reading.** Keep a `#` field of a long-lived entity that a call sets when it starts to its own
|
|
997
|
+
entity, context, promise, or scratch value, or clears when it settles (**Call slots**). The clear
|
|
998
|
+
can sit in a `finally` or in a settle callback the call registers. Judge long-lived by
|
|
999
|
+
**Per-call state**: an owner that can serve another call like the one whose value the field holds
|
|
1000
|
+
is long-lived for it. Keep the hit when the call's steps, its callbacks, an outside handler, or a
|
|
1001
|
+
later call reach the value while the call runs, even when a refusal keeps the calls from
|
|
1002
|
+
overlapping. Drop a bound value, which `bound.field`, `bound.bind`, and `bound.release` rule; a
|
|
1003
|
+
latch and a selection under **Bound values**; and a field a call writes mid-call and never clears,
|
|
1004
|
+
such as `#previous`.
|
|
1005
|
+
|
|
1006
|
+
Hit fixture, `slots.ban.hit.ts`:
|
|
1007
|
+
|
|
1008
|
+
```ts
|
|
1009
|
+
class Worker {
|
|
1010
|
+
readonly #session: Session
|
|
1011
|
+
#meter: RemoteMeter | undefined
|
|
1012
|
+
|
|
1013
|
+
constructor(session: Session) {
|
|
1014
|
+
this.#session = session
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async goal(value: Goal): Promise<Product> {
|
|
1018
|
+
if (this.#meter !== undefined) throw new Error('Worker goal is out of order')
|
|
1019
|
+
const meter = new RemoteMeter()
|
|
1020
|
+
this.#meter = meter
|
|
1021
|
+
try {
|
|
1022
|
+
return await this.#session.execute(value, meter)
|
|
1023
|
+
} finally {
|
|
1024
|
+
this.#meter = undefined
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
append(value: Evidence): Promise<void> {
|
|
1029
|
+
const meter = this.#meter
|
|
1030
|
+
if (meter === undefined) return Promise.reject(new Error('Worker evidence is outside a goal'))
|
|
1031
|
+
return meter.record(value)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
class Conversation {
|
|
1036
|
+
#pending: Promise<unknown> | undefined
|
|
1037
|
+
|
|
1038
|
+
compact(summarize: Summarizer): Promise<Section | undefined> {
|
|
1039
|
+
const previous = this.#pending
|
|
1040
|
+
const fold = this.#fold(previous, summarize)
|
|
1041
|
+
const pending = Promise.allSettled([previous, fold])
|
|
1042
|
+
this.#pending = pending
|
|
1043
|
+
void pending.then(() => {
|
|
1044
|
+
if (this.#pending === pending) this.#pending = undefined
|
|
1045
|
+
})
|
|
1046
|
+
return fold
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async #fold(
|
|
1050
|
+
previous: Promise<unknown> | undefined,
|
|
1051
|
+
summarize: Summarizer,
|
|
1052
|
+
): Promise<Section | undefined> {
|
|
1053
|
+
await previous
|
|
1054
|
+
return summarize()
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
Clear fixture, `slots.ban.clear.ts`:
|
|
1060
|
+
|
|
1061
|
+
```ts
|
|
1062
|
+
class Worker {
|
|
1063
|
+
readonly #meters = new Set<RemoteMeter>()
|
|
1064
|
+
|
|
1065
|
+
async goal(value: Goal): Promise<Product> {
|
|
1066
|
+
if (this.#meters.size !== 0) throw new Error('Worker goal is out of order')
|
|
1067
|
+
const meter = new RemoteMeter()
|
|
1068
|
+
this.#meters.add(meter)
|
|
1069
|
+
try {
|
|
1070
|
+
return await execute(value, meter)
|
|
1071
|
+
} finally {
|
|
1072
|
+
this.#meters.delete(meter)
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
class ConversationManager {
|
|
1078
|
+
readonly #conversations = new Map<string, Conversation>()
|
|
1079
|
+
#active: string | undefined
|
|
1080
|
+
|
|
1081
|
+
switch(id: string): Conversation | undefined {
|
|
1082
|
+
const conversation = this.#conversations.get(id)
|
|
1083
|
+
if (conversation === undefined) return undefined
|
|
1084
|
+
this.#active = id
|
|
1085
|
+
return conversation
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
class Transport {
|
|
1090
|
+
#fault: { readonly error: unknown } | undefined
|
|
1091
|
+
|
|
1092
|
+
fail(error: unknown): void {
|
|
1093
|
+
this.#fault ??= { error }
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
## `resets.ban` and `resets.assignment`, with `bound.state` (**Unit resets**, **Bound values**)
|
|
1099
|
+
|
|
1100
|
+
- **Signatures.**
|
|
1101
|
+
- Fresh initializers: `this\.#[A-Za-z_$][\w$]*\s*=\s*(?:(?:0|-1|false|true|undefined)\b|''|""|\[\]|\{\}|new\s)`.
|
|
1102
|
+
- Emptying: `this\.#[A-Za-z_$][\w$]*\.(?:clear\(\)|length\s*=\s*0|splice\(\s*0)`.
|
|
1103
|
+
- Any assignment inside a method that binds or releases a unit:
|
|
1104
|
+
`this\.#[A-Za-z_$][\w$]*\s*(?:[-+*/%&|^]|\*\*|\?\?|\|\||&&)?=[^=]`.
|
|
1105
|
+
- **Reading.** Keep a hit inside a method that binds or releases a unit of work that later calls
|
|
1106
|
+
continue when it discards the preceding unit's state and initializes state for another unit,
|
|
1107
|
+
whatever its expression reads (**Unit resets**). Read the method: it begins or ends a goal, a
|
|
1108
|
+
session, or a lease. Require that the field is not the bound value's own field and that another
|
|
1109
|
+
method changes it afterwards (`++`, `--`, `+=`, `.push(`, `.set(`, `.add(`, or a reassignment).
|
|
1110
|
+
Keep a field whose lifetime is the unit that sits on the owner (**Bound values**). Drop a field
|
|
1111
|
+
whose state continues across units, such as a cross-goal ordinal or request count; a copy-on-write
|
|
1112
|
+
update that keeps the entity's state; a constructor; teardown; and a reset in a method that
|
|
1113
|
+
neither binds nor releases a unit.
|
|
1114
|
+
|
|
1115
|
+
Hit fixture, `resets.ban.hit.ts`:
|
|
1116
|
+
|
|
1117
|
+
```ts
|
|
1118
|
+
class AgentScript {
|
|
1119
|
+
#plan: Plan | undefined
|
|
1120
|
+
#cursor = 0
|
|
1121
|
+
|
|
1122
|
+
prepare(plan: Plan): void {
|
|
1123
|
+
this.#plan = plan
|
|
1124
|
+
this.#cursor = 0
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
take(): ScriptedResponse | undefined {
|
|
1128
|
+
const response = this.#plan?.responses[this.#cursor]
|
|
1129
|
+
this.#cursor++
|
|
1130
|
+
return response
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
class AgentTrace {
|
|
1135
|
+
#rows: Evidence[] = []
|
|
1136
|
+
#goal: string | undefined
|
|
1137
|
+
|
|
1138
|
+
start(goal: string): void {
|
|
1139
|
+
if (this.#goal !== undefined) throw new Error('Trace goal is already active')
|
|
1140
|
+
this.#rows = []
|
|
1141
|
+
this.#goal = goal
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
append(event: string, value: JSONValue): void {
|
|
1145
|
+
const goal = this.#goal
|
|
1146
|
+
if (goal === undefined) return
|
|
1147
|
+
this.#rows.push({ event, goal, value })
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
class ReplayScript {
|
|
1152
|
+
#plan: Plan | undefined
|
|
1153
|
+
#cursor = 0
|
|
1154
|
+
|
|
1155
|
+
prepare(plan: Plan): void {
|
|
1156
|
+
this.#plan = plan
|
|
1157
|
+
this.#cursor = this.#cursor - this.#cursor
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
take(): ScriptedResponse | undefined {
|
|
1161
|
+
const response = this.#plan?.responses[this.#cursor]
|
|
1162
|
+
this.#cursor += 1
|
|
1163
|
+
return response
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
Clear fixture, `resets.ban.clear.ts`:
|
|
1169
|
+
|
|
1170
|
+
```ts
|
|
1171
|
+
class Channel {
|
|
1172
|
+
#waiting: Array<() => void> = []
|
|
1173
|
+
|
|
1174
|
+
#signal(): void {
|
|
1175
|
+
const waiting = this.#waiting
|
|
1176
|
+
this.#waiting = []
|
|
1177
|
+
for (const wake of waiting) wake()
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
#park(): Promise<void> {
|
|
1181
|
+
return new Promise<void>((resolve) => {
|
|
1182
|
+
this.#waiting = [...this.#waiting, resolve]
|
|
1183
|
+
})
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
class ConversationManager {
|
|
1188
|
+
readonly #conversations = new Map<string, Conversation>()
|
|
1189
|
+
#active: string | undefined
|
|
1190
|
+
|
|
1191
|
+
clear(): void {
|
|
1192
|
+
this.#conversations.clear()
|
|
1193
|
+
this.#active = undefined
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
class Conversation {
|
|
1198
|
+
readonly #rolling: boolean
|
|
1199
|
+
#sections: readonly Section[] = []
|
|
1200
|
+
|
|
1201
|
+
constructor(rolling: boolean) {
|
|
1202
|
+
this.#rolling = rolling
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async #fold(summarize: Summarizer, slice: readonly Message[]): Promise<Section> {
|
|
1206
|
+
const section: Section = { summary: await summarize(slice), messages: slice }
|
|
1207
|
+
this.#sections = this.#rolling ? [section] : [...this.#sections, section]
|
|
1208
|
+
return section
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
class AgentTrace {
|
|
1213
|
+
readonly #goals = new Set<TraceGoal>()
|
|
1214
|
+
#ordinal = 0
|
|
1215
|
+
#dispatches = 0
|
|
1216
|
+
|
|
1217
|
+
start(goal: string): void {
|
|
1218
|
+
if (this.#goals.size !== 0) throw new Error('Trace goal is already active')
|
|
1219
|
+
this.#goals.add(new TraceGoal(goal))
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
admit(): number {
|
|
1223
|
+
return ++this.#ordinal
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
dispatch(): number {
|
|
1227
|
+
return ++this.#dispatches
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
## `bound.field`, with `bound.unit` and `running.position` (**Bound values**)
|
|
1233
|
+
|
|
1234
|
+
- **Signatures.** `const\s*\[\s*[A-Za-z_$][\w$]*\s*\]\s*=\s*this\.#[A-Za-z_$][\w$]*`,
|
|
1235
|
+
`this\.#[A-Za-z_$][\w$]*\.(?:values|keys|entries)\(\)\.next\(\)`, and
|
|
1236
|
+
`\[\s*\.\.\.this\.#[A-Za-z_$][\w$]*\s*\]\s*(?:\[0\]|\.at\()`. For each field, find the methods that
|
|
1237
|
+
add and delete its entry. Also list each explicit bind-to-release pair of methods: a method that
|
|
1238
|
+
sets a `#` field or adds to a `#` collection, and a different later method that releases it.
|
|
1239
|
+
- **Reading.** Keep a `#` collection that holds one bound unit at a time behind a refusal on add,
|
|
1240
|
+
whose entry a later call deletes (`clear`, `close`, or another method's `finally`), and whose unit
|
|
1241
|
+
the entity holds nowhere else (**Bound values**). Drop a registry whose adding call deletes the
|
|
1242
|
+
entry, which `registry.reads` rules; a population, a cache, or a persisted record, which
|
|
1243
|
+
**Registries** excludes; and a selection.
|
|
1244
|
+
|
|
1245
|
+
Hit fixture, `bound.field.hit.ts`:
|
|
1246
|
+
|
|
1247
|
+
```ts
|
|
1248
|
+
class AgentScript {
|
|
1249
|
+
readonly #goals = new Set<ScriptGoal>()
|
|
1250
|
+
readonly #serving = new Set<ScriptedResponse>()
|
|
1251
|
+
|
|
1252
|
+
prepare(plan: Plan): void {
|
|
1253
|
+
if (this.#serving.size !== 0) throw new Error('Cannot replace a running scripted workload')
|
|
1254
|
+
if (this.#goals.size !== 0) throw new Error('Scripted goal is already prepared')
|
|
1255
|
+
this.#goals.add(new ScriptGoal(plan))
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
clear(): void {
|
|
1259
|
+
this.#goals.clear()
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async serve(messages: readonly Message[]): Promise<ProviderResult> {
|
|
1263
|
+
const [goal] = this.#goals
|
|
1264
|
+
if (goal === undefined) throw new Error('Scripted provider is unavailable')
|
|
1265
|
+
const response = goal.take()
|
|
1266
|
+
this.#serving.add(response)
|
|
1267
|
+
try {
|
|
1268
|
+
return await respond(messages, response)
|
|
1269
|
+
} finally {
|
|
1270
|
+
this.#serving.delete(response)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
class AgentTrace {
|
|
1276
|
+
readonly #goals = new Set<TraceGoal>()
|
|
1277
|
+
|
|
1278
|
+
get goal(): string | undefined {
|
|
1279
|
+
const [running] = this.#goals
|
|
1280
|
+
return running?.goal
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
start(goal: string): void {
|
|
1284
|
+
if (this.#goals.size !== 0) throw new Error('Trace goal is already active')
|
|
1285
|
+
this.#goals.add(new TraceGoal(goal))
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async flush(evidence: EvidenceSink): Promise<void> {
|
|
1289
|
+
const [running] = this.#goals
|
|
1290
|
+
if (running === undefined) return
|
|
1291
|
+
try {
|
|
1292
|
+
await running.flush(evidence)
|
|
1293
|
+
} finally {
|
|
1294
|
+
this.#goals.delete(running)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
Clear fixture, `bound.field.clear.ts`:
|
|
1301
|
+
|
|
1302
|
+
```ts
|
|
1303
|
+
class ProcessSession {
|
|
1304
|
+
readonly #goals = new Set<ProcessGoal>()
|
|
1305
|
+
|
|
1306
|
+
async execute(goal: Goal, context: GoalContext): Promise<Product> {
|
|
1307
|
+
if (this.#goals.size !== 0) throw new Error('The process cannot execute in its current state')
|
|
1308
|
+
const running = new ProcessGoal(context)
|
|
1309
|
+
this.#goals.add(running)
|
|
1310
|
+
try {
|
|
1311
|
+
return await request('goal', goal)
|
|
1312
|
+
} finally {
|
|
1313
|
+
this.#goals.delete(running)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async #handle(operation: Operation): Promise<Reply> {
|
|
1318
|
+
const [running] = this.#goals
|
|
1319
|
+
if (running === undefined) throw new Error('The worker requested work outside a goal')
|
|
1320
|
+
return running.context.meter.admit(operation)
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
class MemorySessionStore {
|
|
1325
|
+
readonly #entries = new Map<string, SessionEntry>()
|
|
1326
|
+
readonly #capacity: number
|
|
1327
|
+
|
|
1328
|
+
constructor(capacity: number) {
|
|
1329
|
+
this.#capacity = capacity
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
#reserve(): void {
|
|
1333
|
+
if (this.#entries.size < this.#capacity) return
|
|
1334
|
+
const oldest = this.#entries.keys().next().value
|
|
1335
|
+
if (oldest !== undefined) this.#entries.delete(oldest)
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
## `bound.bind` and `bound.restart` (**Bound values**)
|
|
1341
|
+
|
|
1342
|
+
- **Signatures.** In bind methods (`prepare`, `start`, `begin`, `open`, `bind`, `hold`, `use`), find
|
|
1343
|
+
`this\.#[A-Za-z_$][\w$]*\s*(?:\?\?)?=\s*` and `\.add\(`. Then read the method's guard and its
|
|
1344
|
+
suspensions (`await`, `yield`).
|
|
1345
|
+
- **Reading.** Keep a bind with no throw-while-bound check (`!== undefined` or `size !== 0`, then
|
|
1346
|
+
`throw`), a bind whose check or whose publication of the incoming value comes after the method's
|
|
1347
|
+
first suspension, and a bind that keeps the first value through `??=` instead of throwing
|
|
1348
|
+
(**Bound values**). For a `start` that restarts, keep one that suspends between calling the
|
|
1349
|
+
previous value's end method and publishing the incoming value. Drop a bind that publishes a value
|
|
1350
|
+
owning its asynchronous initialization before its first suspension, and a latch under
|
|
1351
|
+
**Bound values**.
|
|
1352
|
+
|
|
1353
|
+
Hit fixture, `bound.bind.hit.ts`:
|
|
1354
|
+
|
|
1355
|
+
```ts
|
|
1356
|
+
class AuditRegistry {
|
|
1357
|
+
#server: UpstreamServer | undefined
|
|
1358
|
+
|
|
1359
|
+
async start(): Promise<void> {
|
|
1360
|
+
this.#server ??= await createUpstreamServer(AUDIT_REPLIES)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
class GoalRunner {
|
|
1365
|
+
#goal: Goal | undefined
|
|
1366
|
+
|
|
1367
|
+
async begin(plan: Plan): Promise<void> {
|
|
1368
|
+
if (this.#goal !== undefined) throw new Error('A goal is already bound')
|
|
1369
|
+
const goal = await createGoal(plan)
|
|
1370
|
+
this.#goal = goal
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
Clear fixture, `bound.bind.clear.ts`:
|
|
1376
|
+
|
|
1377
|
+
```ts
|
|
1378
|
+
class Holder<TValue> {
|
|
1379
|
+
#value: TValue | undefined
|
|
1380
|
+
|
|
1381
|
+
hold(value: TValue): TValue {
|
|
1382
|
+
if (this.#value !== undefined) throw new Error('The holder already holds a value')
|
|
1383
|
+
this.#value = value
|
|
1384
|
+
return value
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
class AgentTrace {
|
|
1389
|
+
readonly #goals = new Set<TraceGoal>()
|
|
1390
|
+
|
|
1391
|
+
start(goal: string): void {
|
|
1392
|
+
if (this.#goals.size !== 0) throw new Error('Trace goal is already active')
|
|
1393
|
+
this.#goals.add(new TraceGoal(goal))
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
## `bound.release` and `bound.later` (**Bound values**)
|
|
1399
|
+
|
|
1400
|
+
- **Signatures.** For each bound field that `slots.ban`, `bound.field`, or `bound.bind` found, list
|
|
1401
|
+
its `this\.#[A-Za-z_$][\w$]*\s*=\s*undefined`, `\.clear\(\)`, and `\.delete\(` sites by method, and
|
|
1402
|
+
the callbacks and helpers each binding invocation registers or calls.
|
|
1403
|
+
- **Reading.** Keep a binding that only the binding invocation's own settle path, or cleanup that
|
|
1404
|
+
invocation registers or calls, releases, and rule it a call slot under **Call slots**. Keep a
|
|
1405
|
+
binding that no later independent invocation releases while the entity lives on and that is no
|
|
1406
|
+
latch (**Bound values**). Drop a release in `clear`, in teardown, and in a later independent
|
|
1407
|
+
invocation's settle path.
|
|
1408
|
+
|
|
1409
|
+
Hit fixture, `bound.release.hit.ts`:
|
|
1410
|
+
|
|
1411
|
+
```ts
|
|
1412
|
+
class ProcessSession {
|
|
1413
|
+
readonly #channel: Channel
|
|
1414
|
+
#context: GoalContext | undefined
|
|
1415
|
+
|
|
1416
|
+
constructor(channel: Channel) {
|
|
1417
|
+
this.#channel = channel
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
async execute(goal: Goal, context: GoalContext): Promise<Product> {
|
|
1421
|
+
if (this.#context !== undefined)
|
|
1422
|
+
throw new Error('The process cannot execute in its current state')
|
|
1423
|
+
this.#context = context
|
|
1424
|
+
try {
|
|
1425
|
+
return await this.#channel.request('goal', goal)
|
|
1426
|
+
} finally {
|
|
1427
|
+
this.#context = undefined
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async handle(operation: Operation): Promise<Reply> {
|
|
1432
|
+
const context = this.#context
|
|
1433
|
+
if (context === undefined) throw new Error('The worker requested work outside a goal')
|
|
1434
|
+
return context.meter.admit(operation)
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
class Conversation {
|
|
1439
|
+
#pending: Promise<unknown> | undefined
|
|
1440
|
+
|
|
1441
|
+
compact(summarize: Summarizer): Promise<Section | undefined> {
|
|
1442
|
+
const previous = this.#pending
|
|
1443
|
+
const fold = this.#fold(previous, summarize)
|
|
1444
|
+
const pending = Promise.allSettled([previous, fold])
|
|
1445
|
+
this.#pending = pending
|
|
1446
|
+
void pending.then(() => {
|
|
1447
|
+
if (this.#pending === pending) this.#pending = undefined
|
|
1448
|
+
})
|
|
1449
|
+
return fold
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async #fold(
|
|
1453
|
+
previous: Promise<unknown> | undefined,
|
|
1454
|
+
summarize: Summarizer,
|
|
1455
|
+
): Promise<Section | undefined> {
|
|
1456
|
+
await previous
|
|
1457
|
+
return summarize()
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
```
|
|
1461
|
+
|
|
1462
|
+
Clear fixture, `bound.release.clear.ts`:
|
|
1463
|
+
|
|
1464
|
+
```ts
|
|
1465
|
+
class AgentTrace {
|
|
1466
|
+
readonly #goals = new Set<TraceGoal>()
|
|
1467
|
+
readonly #flushes = new Set<Promise<void>>()
|
|
1468
|
+
|
|
1469
|
+
start(goal: string): void {
|
|
1470
|
+
if (this.#goals.size !== 0) throw new Error('Trace goal is already active')
|
|
1471
|
+
this.#goals.add(new TraceGoal(goal))
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async flush(evidence: EvidenceSink): Promise<void> {
|
|
1475
|
+
const [running] = this.#goals
|
|
1476
|
+
if (running === undefined) return
|
|
1477
|
+
if (this.#flushes.size !== 0) throw new Error('Trace flush is already pending')
|
|
1478
|
+
const flushing = running.flush(evidence)
|
|
1479
|
+
this.#flushes.add(flushing)
|
|
1480
|
+
try {
|
|
1481
|
+
await flushing
|
|
1482
|
+
} finally {
|
|
1483
|
+
this.#goals.delete(running)
|
|
1484
|
+
this.#flushes.delete(flushing)
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
## `registry.form` (**Registries**)
|
|
1491
|
+
|
|
1492
|
+
- **Signatures.**
|
|
1493
|
+
- Bulk clears: `this\.#[A-Za-z_$][\w$]*\.clear\(\)` in methods other than `destroy`, `close`,
|
|
1494
|
+
`stop`, and the private teardown they call.
|
|
1495
|
+
- Clear-then-add, with `-U`: `\.clear\(\)\s*\n?\s*this\.#[A-Za-z_$][\w$]*\.add\(`.
|
|
1496
|
+
- Deletes: every `this\.#[A-Za-z_$][\w$]*\.delete\(` on a registry field.
|
|
1497
|
+
- **Reading.** Keep a registry of in-flight entities that a non-teardown method clears in bulk, that
|
|
1498
|
+
a call clears before adding its own entry, or whose entry a path other than that entry's own end
|
|
1499
|
+
deletes (**Registries**). Drop an operation's delete of its own entry, even when it empties the
|
|
1500
|
+
registry, and drop a population, a subscription, a cache, and a persisted record.
|
|
1501
|
+
|
|
1502
|
+
Hit fixture, `registry.form.hit.ts`:
|
|
1503
|
+
|
|
1504
|
+
```ts
|
|
1505
|
+
class Dispatcher {
|
|
1506
|
+
readonly #live = new Set<Call>()
|
|
1507
|
+
|
|
1508
|
+
async dispatch(call: Call): Promise<Reply> {
|
|
1509
|
+
this.#live.clear()
|
|
1510
|
+
this.#live.add(call)
|
|
1511
|
+
try {
|
|
1512
|
+
return await call.run()
|
|
1513
|
+
} finally {
|
|
1514
|
+
this.#live.delete(call)
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
Clear fixture, `registry.form.clear.ts`:
|
|
1521
|
+
|
|
1522
|
+
```ts
|
|
1523
|
+
class ProcessSession {
|
|
1524
|
+
readonly #goals = new Set<ProcessGoal>()
|
|
1525
|
+
readonly #tickets = new Map<number, ParentTicket>()
|
|
1526
|
+
#closing: Promise<void> | undefined
|
|
1527
|
+
|
|
1528
|
+
async execute(goal: Goal, context: GoalContext): Promise<Product> {
|
|
1529
|
+
if (this.#goals.size !== 0) throw new Error('The process cannot execute in its current state')
|
|
1530
|
+
const running = new ProcessGoal(context)
|
|
1531
|
+
this.#goals.add(running)
|
|
1532
|
+
try {
|
|
1533
|
+
return await request('goal', goal)
|
|
1534
|
+
} finally {
|
|
1535
|
+
this.#goals.delete(running)
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
close(): Promise<void> {
|
|
1540
|
+
this.#closing ??= this.#close()
|
|
1541
|
+
return this.#closing
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
async #close(): Promise<void> {
|
|
1545
|
+
for (const entry of this.#tickets.values()) entry.listeners.abort()
|
|
1546
|
+
this.#tickets.clear()
|
|
1547
|
+
await terminate()
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
## `registry.reads` and `registry.position`, with `running.position` (**Registries**)
|
|
1553
|
+
|
|
1554
|
+
- **Signatures.** The `bound.field` position-read signatures, `keys()` and `entries()` iterator reads
|
|
1555
|
+
and a key obtained that way included, restricted to registry fields.
|
|
1556
|
+
- **Reading.** Keep every position read unless all of these hold (**Registries**):
|
|
1557
|
+
- a `size !== 0` throw precedes every add to that registry;
|
|
1558
|
+
- the call that added the entry deletes it on every path that settles that call;
|
|
1559
|
+
- the read handles the empty registry.
|
|
1560
|
+
|
|
1561
|
+
Drop a `size` read, a whole read, and a keyed read.
|
|
1562
|
+
|
|
1563
|
+
Hit fixture, `registry.reads.hit.ts`:
|
|
1564
|
+
|
|
1565
|
+
```ts
|
|
1566
|
+
class Supervisor {
|
|
1567
|
+
readonly #children = new Set<ChildProcess>()
|
|
1568
|
+
|
|
1569
|
+
spawn(command: string): ChildProcess {
|
|
1570
|
+
const child = spawnChild(command)
|
|
1571
|
+
this.#children.add(child)
|
|
1572
|
+
child.once('exit', () => this.#children.delete(child))
|
|
1573
|
+
return child
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
first(): ChildProcess | undefined {
|
|
1577
|
+
const [child] = this.#children
|
|
1578
|
+
return child
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
oldest(): ChildProcess | undefined {
|
|
1582
|
+
return this.#children.values().next().value
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
Clear fixture, `registry.reads.clear.ts`:
|
|
1588
|
+
|
|
1589
|
+
```ts
|
|
1590
|
+
class Worker {
|
|
1591
|
+
readonly #meters = new Set<RemoteMeter>()
|
|
1592
|
+
|
|
1593
|
+
async goal(value: Goal): Promise<Product> {
|
|
1594
|
+
if (this.#meters.size !== 0) throw new Error('Worker goal is invalid or out of order')
|
|
1595
|
+
const meter = new RemoteMeter()
|
|
1596
|
+
this.#meters.add(meter)
|
|
1597
|
+
try {
|
|
1598
|
+
return await execute(value, meter)
|
|
1599
|
+
} finally {
|
|
1600
|
+
this.#meters.delete(meter)
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
abort(value: Cancellation): void {
|
|
1605
|
+
const [meter] = this.#meters
|
|
1606
|
+
if (meter === undefined) throw new Error('Worker ticket cancellation is foreign')
|
|
1607
|
+
meter.abort(value.id, value.failure)
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
class ProcessSession {
|
|
1612
|
+
readonly #goals = new Set<ProcessGoal>()
|
|
1613
|
+
|
|
1614
|
+
async execute(goal: Goal, context: GoalContext): Promise<Product> {
|
|
1615
|
+
if (this.#goals.size !== 0) throw new Error('The process cannot execute in its current state')
|
|
1616
|
+
const running = new ProcessGoal(context)
|
|
1617
|
+
this.#goals.add(running)
|
|
1618
|
+
try {
|
|
1619
|
+
return await request('goal', goal)
|
|
1620
|
+
} finally {
|
|
1621
|
+
this.#goals.delete(running)
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
async #handle(operation: Operation): Promise<Reply> {
|
|
1626
|
+
const [running] = this.#goals
|
|
1627
|
+
if (running === undefined) throw new Error('The worker requested work outside a goal')
|
|
1628
|
+
return running.context.meter.admit(operation)
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
## `exemptions.membership` and `exemptions.walk` (**Exemptions**)
|
|
1634
|
+
|
|
1635
|
+
- **Signatures.** `const\s+[A-Za-z_$][\w$]*\s*(?::[^=]+)?=\s*new (?:Set|Map)\b`. For each, list the
|
|
1636
|
+
name's `\.has\(` reads and its `\.(?:add|set)\(` writes inside the loops of its function.
|
|
1637
|
+
- **Reading.** Keep a set or map that a loop reads through `has` and writes through `add` or `set`
|
|
1638
|
+
when the loop is no traversal: it reaches no node as a successor of a node it has already reached
|
|
1639
|
+
(**Exemptions**). Drop a traversal's membership set or map, whether or not the traversal iterates
|
|
1640
|
+
it, and a map that `exemptions.memo` rules as a memo.
|
|
1641
|
+
|
|
1642
|
+
Hit fixture, `exemptions.membership.hit.ts`:
|
|
1643
|
+
|
|
1644
|
+
```ts
|
|
1645
|
+
function selectPlugins(
|
|
1646
|
+
base: readonly PluginOption[],
|
|
1647
|
+
candidates: readonly PluginOption[],
|
|
1648
|
+
): readonly PluginOption[] {
|
|
1649
|
+
const taken = new Set<number>()
|
|
1650
|
+
const selected: PluginOption[] = []
|
|
1651
|
+
for (const plugin of base) {
|
|
1652
|
+
const index = candidates.findIndex(
|
|
1653
|
+
(candidate, position) =>
|
|
1654
|
+
!taken.has(position) && isNamedPlugin(candidate) && candidate.name === readName(plugin),
|
|
1655
|
+
)
|
|
1656
|
+
const replacement = candidates[index]
|
|
1657
|
+
if (replacement === undefined) {
|
|
1658
|
+
selected.push(plugin)
|
|
1659
|
+
} else {
|
|
1660
|
+
selected.push(replacement)
|
|
1661
|
+
taken.add(index)
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
for (const [index, plugin] of candidates.entries()) {
|
|
1665
|
+
if (!taken.has(index)) selected.push(plugin)
|
|
1666
|
+
}
|
|
1667
|
+
return selected
|
|
1668
|
+
}
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
Clear fixture, `exemptions.membership.clear.ts`:
|
|
1672
|
+
|
|
1673
|
+
```ts
|
|
1674
|
+
function collectDescendants(rows: readonly (readonly number[])[], root: number): readonly number[] {
|
|
1675
|
+
const descendants = new Set([root])
|
|
1676
|
+
rows.forEach(() => {
|
|
1677
|
+
for (const [pid, parent] of rows)
|
|
1678
|
+
if (pid !== undefined && parent !== undefined && descendants.has(parent)) descendants.add(pid)
|
|
1679
|
+
})
|
|
1680
|
+
return [...descendants].reverse()
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
function scanAmendments(
|
|
1684
|
+
amendments: ReadonlyMap<string, readonly string[]>,
|
|
1685
|
+
start: string,
|
|
1686
|
+
admit: (id: string) => boolean,
|
|
1687
|
+
): readonly string[] {
|
|
1688
|
+
const queue = [start]
|
|
1689
|
+
const seen = new Set<string>()
|
|
1690
|
+
const admitted: string[] = []
|
|
1691
|
+
for (const id of queue) {
|
|
1692
|
+
if (seen.has(id)) continue
|
|
1693
|
+
seen.add(id)
|
|
1694
|
+
if (!admit(id)) continue
|
|
1695
|
+
admitted.push(id)
|
|
1696
|
+
queue.push(...(amendments.get(id) ?? []))
|
|
1697
|
+
}
|
|
1698
|
+
return admitted
|
|
1699
|
+
}
|
|
1700
|
+
```
|