@orkestrel/scaffold 0.0.100 → 0.0.102
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/brief.js +6 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
- package/dist/bin/main.js +483 -444
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
- package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
- package/dist/host/claude/rules/architecture.md +2 -2
- package/dist/host/claude/rules/tests.md +8 -2
- package/dist/host/claude/rules/typescript.md +130 -0
- package/dist/host/claude/rules/workspace.md +36 -12
- package/dist/host/configs/helpers.ts +214 -133
- package/dist/host/configs/policy.ts +110 -105
- package/dist/host/dotfiles/gitignore +1 -0
- package/dist/host/dotfiles/oxlintrc.json +10 -0
- package/dist/host/guides/scaffold.md +244 -170
- package/dist/host/manifest.json +37 -31
- package/dist/host/tests/config.test.ts +1305 -619
- package/dist/host/tests/policy.test.ts +19 -0
- package/dist/host/tests/setupPolicy.ts +1710 -995
- package/dist/src/core/index.cjs +1595 -1686
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +202 -18
- package/dist/src/core/index.d.ts +202 -18
- package/dist/src/core/index.js +1590 -1687
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +448 -392
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +1182 -1052
- package/dist/src/server/index.d.ts +1182 -1052
- package/dist/src/server/index.js +447 -395
- package/dist/src/server/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -53,6 +53,31 @@ interface AnchorRow {
|
|
|
53
53
|
readonly heading: string
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
interface TomlState {
|
|
57
|
+
readonly table: string
|
|
58
|
+
readonly fence: string | undefined
|
|
59
|
+
readonly rows: readonly LineRow[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface YamlLine {
|
|
63
|
+
readonly line: number
|
|
64
|
+
readonly indent: number
|
|
65
|
+
readonly content: string
|
|
66
|
+
readonly key: string | undefined
|
|
67
|
+
readonly value: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface YamlHead {
|
|
71
|
+
readonly position: number
|
|
72
|
+
readonly line: YamlLine
|
|
73
|
+
readonly key: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface YamlState {
|
|
77
|
+
readonly stack: ReadonlyArray<{ readonly indent: number; readonly key: string }>
|
|
78
|
+
readonly heads: readonly YamlHead[]
|
|
79
|
+
}
|
|
80
|
+
|
|
56
81
|
interface Report {
|
|
57
82
|
readonly tree?: readonly TreeRow[]
|
|
58
83
|
readonly headings?: readonly LineRow[]
|
|
@@ -93,35 +118,42 @@ function mapTree(paths: readonly string[]): readonly TreeRow[] {
|
|
|
93
118
|
.sort((left, right) => left.path.localeCompare(right.path))
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
function scanFences(lines: readonly string[]): readonly boolean[] {
|
|
122
|
+
const fenced: boolean[] = []
|
|
123
|
+
for (const text of lines) fenced.push((fenced.at(-1) ?? false) !== text.startsWith('```'))
|
|
124
|
+
return fenced
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
function mapHeadings(paths: readonly string[]): readonly LineRow[] {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
return expandPaths(paths)
|
|
129
|
+
.filter((candidate) => candidate.endsWith('.md'))
|
|
130
|
+
.flatMap((path) => {
|
|
131
|
+
const lines = readLines(path)
|
|
132
|
+
const fenced = scanFences(lines)
|
|
133
|
+
return lines.flatMap((text, index) => {
|
|
134
|
+
if (fenced[index] === true) return []
|
|
135
|
+
const match = text.match(HEADING)
|
|
136
|
+
return match?.[1] !== undefined && match[2] !== undefined
|
|
137
|
+
? [{ path, line: index + 1, text: `${match[1]} ${match[2]}` }]
|
|
138
|
+
: []
|
|
139
|
+
})
|
|
107
140
|
})
|
|
108
|
-
}
|
|
109
|
-
return rows
|
|
110
141
|
}
|
|
111
142
|
|
|
112
143
|
function mapExports(paths: readonly string[]): readonly LineRow[] {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
return expandPaths(paths)
|
|
145
|
+
.filter((candidate) => /\.[cm]?ts$/u.test(candidate))
|
|
146
|
+
.flatMap((path) =>
|
|
147
|
+
readLines(path).flatMap((text, index) => {
|
|
148
|
+
const match = text.match(EXPORT)
|
|
149
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
150
|
+
return [{ path, line: index + 1, text: `${match[1]} ${match[2]}` }]
|
|
151
|
+
}
|
|
152
|
+
return /^export\s+\*\s+from\s+/u.test(text)
|
|
153
|
+
? [{ path, line: index + 1, text: text.trim() }]
|
|
154
|
+
: []
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
125
157
|
}
|
|
126
158
|
|
|
127
159
|
function buildMatcher(term: string, ignoreCase: boolean): (text: string) => boolean {
|
|
@@ -137,102 +169,129 @@ function mapCensus(
|
|
|
137
169
|
const files = expandPaths(roots).filter(isText)
|
|
138
170
|
return terms.map((term) => {
|
|
139
171
|
const matches = buildMatcher(term, ignoreCase)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
return {
|
|
173
|
+
term,
|
|
174
|
+
rows: files.flatMap((path) =>
|
|
175
|
+
readLines(path).flatMap((text, index) =>
|
|
176
|
+
matches(text) ? [{ path, line: index + 1, text: text.trim().slice(0, 160) }] : [],
|
|
177
|
+
),
|
|
178
|
+
),
|
|
145
179
|
}
|
|
146
|
-
return { term, rows }
|
|
147
180
|
})
|
|
148
181
|
}
|
|
149
182
|
|
|
150
183
|
function mapRefs(target: string, roots: readonly string[]): readonly LineRow[] {
|
|
151
184
|
const needle = normalizeSlashes(target)
|
|
152
185
|
const name = basename(needle)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
186
|
+
return expandPaths(roots)
|
|
187
|
+
.filter(isText)
|
|
188
|
+
.filter((path) => path !== needle)
|
|
189
|
+
.flatMap((path) =>
|
|
190
|
+
readLines(path).flatMap((text, index) =>
|
|
191
|
+
text.includes(needle) || text.includes(name)
|
|
192
|
+
? [{ path, line: index + 1, text: text.trim().slice(0, 160) }]
|
|
193
|
+
: [],
|
|
194
|
+
),
|
|
195
|
+
)
|
|
163
196
|
}
|
|
164
197
|
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
const stack: Array<{ readonly indent: number; readonly key: string }> = []
|
|
168
|
-
lines.forEach((text, index) => {
|
|
198
|
+
function classifyYamlLines(lines: readonly string[], offset: number): readonly YamlLine[] {
|
|
199
|
+
return lines.flatMap((text, index): readonly YamlLine[] => {
|
|
169
200
|
const content = text.trimStart()
|
|
170
|
-
if (content === '' || content.startsWith('#')) return
|
|
171
|
-
const indent = text.length - content.length
|
|
201
|
+
if (content === '' || content.startsWith('#')) return []
|
|
172
202
|
const match = content.match(/^([A-Za-z_][\w-]*)\s*:(?:\s+(.*))?$/u)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const key = [...stack.map((entry) => entry.key), match[1]].join('.')
|
|
183
|
-
stack.push({ indent, key: match[1] })
|
|
184
|
-
rows.push({ path, line: index + offset, text: `${key}: ${(match[2] ?? '').trim()}`.trimEnd() })
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
line: index + offset,
|
|
206
|
+
indent: text.length - content.length,
|
|
207
|
+
content,
|
|
208
|
+
key: match?.[1],
|
|
209
|
+
value: (match?.[2] ?? '').trim(),
|
|
210
|
+
},
|
|
211
|
+
]
|
|
185
212
|
})
|
|
186
|
-
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function stepYamlState(state: YamlState, line: YamlLine, position: number): YamlState {
|
|
216
|
+
if (line.key === undefined) return state
|
|
217
|
+
// Indents strictly increase along the stack, so the entries at or past this indent are the tail.
|
|
218
|
+
const stack = state.stack.filter((entry) => entry.indent < line.indent)
|
|
219
|
+
const key = [...stack.map((entry) => entry.key), line.key].join('.')
|
|
220
|
+
return {
|
|
221
|
+
stack: [...stack, { indent: line.indent, key: line.key }],
|
|
222
|
+
heads: [...state.heads, { position, line, key }],
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readYamlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
|
|
227
|
+
const classified = classifyYamlLines(lines, offset)
|
|
228
|
+
const { heads } = classified.reduce<YamlState>(
|
|
229
|
+
(state, line, position) => stepYamlState(state, line, position),
|
|
230
|
+
{ stack: [], heads: [] },
|
|
231
|
+
)
|
|
232
|
+
// A list item, a block scalar line, or a wrapped value extends the key line before it when it is
|
|
233
|
+
// indented at least as far; between two key lines every classified line is a non-key line.
|
|
234
|
+
return heads.map((head, rank) => ({
|
|
235
|
+
path,
|
|
236
|
+
line: head.line.line,
|
|
237
|
+
text: [
|
|
238
|
+
`${head.key}: ${head.line.value}`.trimEnd(),
|
|
239
|
+
...classified
|
|
240
|
+
.slice(head.position + 1, heads[rank + 1]?.position ?? classified.length)
|
|
241
|
+
.filter((line) => line.indent >= head.line.indent)
|
|
242
|
+
.map((line) => line.content.trim()),
|
|
243
|
+
]
|
|
244
|
+
.join(' ')
|
|
245
|
+
.trimEnd(),
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function stepTomlState(state: TomlState, text: string, path: string, line: number): TomlState {
|
|
250
|
+
if (state.fence !== undefined) {
|
|
251
|
+
// The body of a multi-line string carries no key.
|
|
252
|
+
return text.includes(state.fence) ? { ...state, fence: undefined } : state
|
|
253
|
+
}
|
|
254
|
+
const header = text.match(/^\s*\[\[?([^\]]+)\]\]?\s*$/u)
|
|
255
|
+
if (header?.[1] !== undefined) return { ...state, table: header[1].trim() }
|
|
256
|
+
const match = text.match(/^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.*)$/u)
|
|
257
|
+
if (match?.[1] === undefined) return state
|
|
258
|
+
const value = (match[2] ?? '').trim()
|
|
259
|
+
const opening = value.match(/^("""|''')/u)?.[1]
|
|
260
|
+
return {
|
|
261
|
+
table: state.table,
|
|
262
|
+
fence: opening !== undefined && value.indexOf(opening, 3) === -1 ? opening : undefined,
|
|
263
|
+
rows: [
|
|
264
|
+
...state.rows,
|
|
265
|
+
{
|
|
266
|
+
path,
|
|
267
|
+
line,
|
|
268
|
+
text: `${state.table === '' ? '' : `${state.table}.`}${match[1]}: ${value}`.trimEnd(),
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
}
|
|
187
272
|
}
|
|
188
273
|
|
|
189
274
|
function readTomlKeys(path: string, lines: readonly string[], offset: number): readonly LineRow[] {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (fence !== undefined) {
|
|
195
|
-
// The body of a multi-line string carries no key.
|
|
196
|
-
if (text.includes(fence)) fence = undefined
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
const header = text.match(/^\s*\[\[?([^\]]+)\]\]?\s*$/u)
|
|
200
|
-
if (header?.[1] !== undefined) {
|
|
201
|
-
table = header[1].trim()
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
const match = text.match(/^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.*)$/u)
|
|
205
|
-
if (match?.[1] === undefined) return
|
|
206
|
-
const value = (match[2] ?? '').trim()
|
|
207
|
-
const opening = value.match(/^("""|''')/u)?.[1]
|
|
208
|
-
if (opening !== undefined && value.indexOf(opening, 3) === -1) fence = opening
|
|
209
|
-
rows.push({
|
|
210
|
-
path,
|
|
211
|
-
line: index + offset,
|
|
212
|
-
text: `${table === '' ? '' : `${table}.`}${match[1]}: ${value}`.trimEnd(),
|
|
213
|
-
})
|
|
214
|
-
})
|
|
215
|
-
return rows
|
|
275
|
+
return lines.reduce<TomlState>(
|
|
276
|
+
(state, text, index) => stepTomlState(state, text, path, index + offset),
|
|
277
|
+
{ table: '', fence: undefined, rows: [] },
|
|
278
|
+
).rows
|
|
216
279
|
}
|
|
217
280
|
|
|
218
281
|
function mapFrontmatter(paths: readonly string[]): readonly LineRow[] {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
rows.push(
|
|
230
|
-
...(path.endsWith('.toml')
|
|
282
|
+
return expandPaths(paths)
|
|
283
|
+
.filter((candidate) => /\.(md|mdc|toml|ya?ml)$/u.test(candidate))
|
|
284
|
+
.flatMap((path) => {
|
|
285
|
+
const lines = readLines(path)
|
|
286
|
+
const markdown = /\.mdc?$/u.test(path)
|
|
287
|
+
const end = markdown ? lines.indexOf('---', 1) : lines.length
|
|
288
|
+
if (markdown && (lines[0] !== '---' || end === -1)) return []
|
|
289
|
+
const body = lines.slice(markdown ? 1 : 0, end)
|
|
290
|
+
const offset = markdown ? 2 : 1
|
|
291
|
+
return path.endsWith('.toml')
|
|
231
292
|
? readTomlKeys(path, body, offset)
|
|
232
|
-
: readYamlKeys(path, body, offset)
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
return rows
|
|
293
|
+
: readYamlKeys(path, body, offset)
|
|
294
|
+
})
|
|
236
295
|
}
|
|
237
296
|
|
|
238
297
|
function readHeadingTexts(path: string): ReadonlySet<string> {
|
|
@@ -240,63 +299,64 @@ function readHeadingTexts(path: string): ReadonlySet<string> {
|
|
|
240
299
|
}
|
|
241
300
|
|
|
242
301
|
function mapAnchors(paths: readonly string[]): readonly AnchorRow[] {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
302
|
+
const citations = expandPaths(paths)
|
|
303
|
+
.filter((candidate) => candidate.endsWith('.md'))
|
|
304
|
+
.flatMap((path) =>
|
|
305
|
+
readLines(path).flatMap((text, index) =>
|
|
306
|
+
[...text.matchAll(ANCHOR)].flatMap((match) => {
|
|
307
|
+
const target = match[1] === undefined ? path : normalizeSlashes(match[1])
|
|
308
|
+
const heading = (match[2] ?? '').trim()
|
|
309
|
+
return heading === '' ? [] : [{ path, line: index + 1, target, heading }]
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
// Each existing target is read once, in the order its first citation names it.
|
|
314
|
+
const headings = new Map(
|
|
315
|
+
[...new Set(citations.map(({ target }) => target).filter((target) => existsSync(target)))].map(
|
|
316
|
+
(target) => [target, readHeadingTexts(target)],
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
return citations.filter(
|
|
320
|
+
({ target, heading }) => headings.get(target)?.has(heading.toLowerCase()) !== true,
|
|
321
|
+
)
|
|
262
322
|
}
|
|
263
323
|
|
|
264
324
|
function renderReport(report: Report): string {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
325
|
+
const { tree, headings, frontmatter, exports, census, refs, anchors } = report
|
|
326
|
+
const lines = [
|
|
327
|
+
...(tree === undefined
|
|
328
|
+
? []
|
|
329
|
+
: [
|
|
330
|
+
'## Tree',
|
|
331
|
+
...tree.map((row) => `${row.path}\t${row.lines} lines\t${row.bytes} bytes`),
|
|
332
|
+
`total\t${tree.length} files\t${tree.reduce((sum, row) => sum + row.lines, 0)} lines`,
|
|
333
|
+
]),
|
|
334
|
+
...(headings === undefined
|
|
335
|
+
? []
|
|
336
|
+
: ['## Headings', ...headings.map((row) => `${row.path}:${row.line}\t${row.text}`)]),
|
|
337
|
+
...(frontmatter === undefined
|
|
338
|
+
? []
|
|
339
|
+
: ['## Frontmatter', ...frontmatter.map((row) => `${row.path}:${row.line}\t${row.text}`)]),
|
|
340
|
+
...(exports === undefined
|
|
341
|
+
? []
|
|
342
|
+
: [
|
|
343
|
+
'## Exports (text reading)',
|
|
344
|
+
...exports.map((row) => `${row.path}:${row.line}\t${row.text}`),
|
|
345
|
+
]),
|
|
346
|
+
...(census ?? []).flatMap((entry) => [
|
|
347
|
+
`## Census: ${entry.term} (${entry.rows.length})`,
|
|
348
|
+
...entry.rows.map((row) => `${row.path}:${row.line}\t${row.text}`),
|
|
349
|
+
]),
|
|
350
|
+
...(refs === undefined
|
|
351
|
+
? []
|
|
352
|
+
: [`## Refs (${refs.length})`, ...refs.map((row) => `${row.path}:${row.line}\t${row.text}`)]),
|
|
353
|
+
...(anchors === undefined
|
|
354
|
+
? []
|
|
355
|
+
: [
|
|
356
|
+
`## Unresolved anchors (${anchors.length})`,
|
|
357
|
+
...anchors.map((row) => `${row.path}:${row.line}\t${row.target} § ${row.heading}`),
|
|
358
|
+
]),
|
|
359
|
+
]
|
|
300
360
|
return `${lines.join('\n')}\n`
|
|
301
361
|
}
|
|
302
362
|
|
|
@@ -203,8 +203,8 @@ Store child managers in `#` fields and expose readonly getters typed as their in
|
|
|
203
203
|
|
|
204
204
|
- Middleware is behavior `(context, next) => ...`, never a class with one `handle` method.
|
|
205
205
|
- Place middleware factories in `middlewares.ts` as `createX(options): Middleware`; keep them distinct from entity/value factories.
|
|
206
|
-
- Keep middleware
|
|
207
|
-
-
|
|
206
|
+
- Keep the values a middleware never changes, such as its resolved options and what its factory derives from them, closure-private, and apply `.claude/rules/typescript.md` § Bindings to the state one request changes.
|
|
207
|
+
- Keep the state a middleware changes across requests, such as a per-key count, a cache, or a session table, in a pluggable class supplied as an option, with an in-memory default.
|
|
208
208
|
- Extract reusable cross-middleware machinery to helpers/classes.
|
|
209
209
|
- `MiddlewareManager` is the sole manager of the middleware composition chain; do not create one manager per middleware.
|
|
210
210
|
|
|
@@ -22,7 +22,9 @@ paths:
|
|
|
22
22
|
resolves to `tests/setup<Name>.ts`. A root `tests/setup<Name>.ts` that declares an export has
|
|
23
23
|
`tests/setup<Name>.test.ts` or is imported by `tests/setup.test.ts`, which can prove several
|
|
24
24
|
setup modules when their helpers serve several projects. A vendored module is outside this
|
|
25
|
-
population. The policy sweep (`tests/setupPolicy.ts`) enforces both directions.
|
|
25
|
+
population. The policy sweep (`tests/setupPolicy.ts`) enforces both directions. `tests/studies/**`
|
|
26
|
+
is outside the mirrored population: a study test names a package-level subject and mirrors no
|
|
27
|
+
module.
|
|
26
28
|
- Prefer test filenames matching entrypoints: `index.test.ts` for `index.ts`, `main.test.ts` for `main.ts`.
|
|
27
29
|
- Tests are deterministic: identical inputs produce identical results.
|
|
28
30
|
- Keep default suites fast: give each timer the shortest real duration that still separates the orderings the test asserts on a loaded host, and make no network calls.
|
|
@@ -57,6 +59,7 @@ its own:
|
|
|
57
59
|
| `tests/guides.test.ts` | Every documented API exists, every public API is documented, every compared summary, example, and pitch equals its source, and every executable fence returns what the guide says it returns |
|
|
58
60
|
| `tests/conformance.test.ts` | Where this package drifts from the official tooling it tracks |
|
|
59
61
|
| `tests/agents/**/*.test.ts` | Each skill script does what its `SKILL.md` states, driven as a child process against a scratch fixture from its mirrored proof; the scaffold checkout alone carries them |
|
|
62
|
+
| `tests/studies/**/*.test.ts` | Evaluations of the workspace's consumer study code under `studies/`, run through bounded subprocesses and protocol fixtures; the `test:studies` script is their gate |
|
|
60
63
|
| `tests/distribution.test.ts` | The packed package installs and resolves through its public exports |
|
|
61
64
|
| `tests/integration.test.ts` | The package's features work together end to end across environments |
|
|
62
65
|
| `tests/setup*.test.ts` | Reusable behavior exported from sibling `tests/setup*.ts` modules works as the workspace's suites require |
|
|
@@ -167,7 +170,7 @@ A test that spawns a process, packs, installs, or drives a real build is a proof
|
|
|
167
170
|
- Keep it out of the default run where the workspace has a gate that can hold it.
|
|
168
171
|
`.claude/rules/workspace.md` fixes which gate each isolated project runs from, and that placement
|
|
169
172
|
differs between a publishing and a `private: true` workspace.
|
|
170
|
-
- The fixed expensive-proof projects are `distribution` and `
|
|
173
|
+
- The fixed expensive-proof projects are `distribution`, `service`, and `studies`.
|
|
171
174
|
- A `distribution` proof reads `import.meta.env.MODE` and fails, rather than skips, on an unreachable
|
|
172
175
|
registry under `--mode release`. The publish gate invokes it that way, so a proof that skips there
|
|
173
176
|
passes the gate without ever proving the artifact installs. An ordinary local run may still skip.
|
|
@@ -201,6 +204,9 @@ Place helpers by environment:
|
|
|
201
204
|
- `tests/setupBrowser.ts`: DOM/Vue/browser helpers and setup CSS.
|
|
202
205
|
- `tests/setupStyles.ts`: the CSSOM and sheet helpers `.claude/rules/styles.md` places there; every
|
|
203
206
|
sheet-face project loads it after `tests/setupBrowser.ts`.
|
|
207
|
+
- `tests/setupStudy.ts`: the harness module the study tests import and, in a study-engine package,
|
|
208
|
+
its source tests too. Name it in no project's `setupFiles`; tests import it, as the policy proof
|
|
209
|
+
imports `tests/setupPolicy.ts`.
|
|
204
210
|
|
|
205
211
|
### Recorder
|
|
206
212
|
|
|
@@ -41,6 +41,136 @@ The non-negotiables and design laws in `AGENTS.md` apply without exception and a
|
|
|
41
41
|
- Return copies or readonly views; never leak a mutable internal reference.
|
|
42
42
|
- Compute derived facts instead of persisting duplicate state.
|
|
43
43
|
|
|
44
|
+
## Bindings
|
|
45
|
+
|
|
46
|
+
- **Variables.** Never write a `let` or `var` declaration, at module scope, in a function, in a loop
|
|
47
|
+
head, or in a test; declare each variable with the `const` keyword. Never assign to a parameter or
|
|
48
|
+
a `catch` binding, whether by `=`, by a compound operator such as `+=` or `??=`, by `++` or `--`,
|
|
49
|
+
or as a destructuring target.
|
|
50
|
+
- **Derivation.** Derive a value instead of accumulating it: write the expression that produces it
|
|
51
|
+
(`some`, `every`, `find`, `filter`, `map`, `reduce`, a conditional expression), or extract a
|
|
52
|
+
`{verb}{Noun}` helper that returns it. Build text from its parts with `join` or a template literal,
|
|
53
|
+
never with a fold whose step appends a part to the text it carries.
|
|
54
|
+
- **Iteration.** Iterate with `for…of` over `entries()` or an iterable instead of an index counter.
|
|
55
|
+
- **Try results.** Take a value out of a `try` block through a helper that runs the block and
|
|
56
|
+
returns the value. Treat a log that the block appends to as a log rather than such a value, and
|
|
57
|
+
apply the **Lists and logs** rule to it.
|
|
58
|
+
- **Running values.** Treat a flag, a counter, a sum, a latest value, a one-slot cell, or text built
|
|
59
|
+
piece by piece as a running value. Treat carrying one by changing a record, an array, a `Map`, a
|
|
60
|
+
`Set`, or a recorder as a reassigned binding, whether a variable, a parameter, or a `reduce`
|
|
61
|
+
accumulator holds that collection, directly or through a property: derive the value, or keep it
|
|
62
|
+
where the **Per-call state** or **Entity state** rule places it. Treat a collection that holds at
|
|
63
|
+
most one entry and that code reads by position, such as `cell[0]`, `cell.at(-1)`, or
|
|
64
|
+
`const [entry] = cell`, as a one-slot cell. Treat selecting an entry by iteration order as a
|
|
65
|
+
position read, including an iterator read such as `values().next()` or `keys().next()`, and a key
|
|
66
|
+
obtained that way.
|
|
67
|
+
- **Lists and logs.** Build a list from a list with `map`, `filter`, or `flatMap`, and a lookup with
|
|
68
|
+
`new Map(…)` or `new Set(…)` over a derived list. When a loop awaits or yields between steps, or
|
|
69
|
+
its step depends on earlier steps, append each step's readonly result to a log that the loop owns,
|
|
70
|
+
and derive every aggregate from the log, never from a running value. Apply these conditions whether
|
|
71
|
+
the loop pushes to the log or a `reduce` call returns it:
|
|
72
|
+
- let only the loop's own steps, in order, or the callbacks of one event source, in emission order,
|
|
73
|
+
append to the log, and apply the **Per-call state** rule to a list that concurrent paths append
|
|
74
|
+
to;
|
|
75
|
+
- let a step read the record before it and carry an aggregate computed from that record, such as a
|
|
76
|
+
running total;
|
|
77
|
+
- append each record final: never replace, rewrite, or remove a record the log holds, and never
|
|
78
|
+
append a record whose text extends an earlier record's text;
|
|
79
|
+
- treat a log, or a `Map` keyed by step, whose every read takes its last entry or its length as the
|
|
80
|
+
carrier of a latest value or a counter: derive that value from the source list, return it from a
|
|
81
|
+
bounded recursion, or carry it in a `reduce` accumulator;
|
|
82
|
+
- find a log's reads through its aliases, the calls it is passed to, and the callers it is returned
|
|
83
|
+
to, and never count passing or returning the log as a read of its history.
|
|
84
|
+
- **Exemptions.** Exempt these from the **Running values**, **Lists and logs**, and **Per-call state**
|
|
85
|
+
rules:
|
|
86
|
+
- a traversal's worklist, which is a list, `Set`, or `Map` that a loop visits while its steps add
|
|
87
|
+
the entries left to visit, provided one step can add more than one entry. Treat a loop as a
|
|
88
|
+
traversal only when it reaches each node after its roots as a successor of a node it has already
|
|
89
|
+
reached, such as a child directory, a dependency, an amendment, or a child process. Count as one
|
|
90
|
+
step a complete iteration that removes or visits an entry of the worklist, its nested loops
|
|
91
|
+
included. Treat a worklist that no step can add more than one entry to as a cursor, and return
|
|
92
|
+
what it reaches from a bounded recursion;
|
|
93
|
+
- a traversal's membership set or map, which the traversal reads to avoid revisiting a node or to
|
|
94
|
+
discover the nodes reachable from nodes it has recorded, whether or not it iterates that
|
|
95
|
+
collection, and to which the cursor condition never applies;
|
|
96
|
+
- a memo, which is a `Map` that caches a computation's result by its input or identity key for
|
|
97
|
+
reuse within one owning operation, writes each key once, and never changes a stored value. Never
|
|
98
|
+
key a memo by iteration position: treat a `Map` keyed by step as a log;
|
|
99
|
+
- a list or record that straight-line branches outside any loop append to, with no entry written
|
|
100
|
+
twice;
|
|
101
|
+
- a write that a platform API requires, such as `process.exitCode`, a DOM property, or a Vue `ref`
|
|
102
|
+
value, and one write per key into a fresh local object that builds a fixture a literal cannot
|
|
103
|
+
express, such as a cycle or a sparse array.
|
|
104
|
+
- **Per-call state.** Keep state that the awaits or callbacks of one call change, other than a log,
|
|
105
|
+
in `#` fields of an entity created for that call. Treat a list that more than one concurrently
|
|
106
|
+
pending async path of one call appends to, such as the workers of a pool, as state of that call:
|
|
107
|
+
collect each path's result through `Promise.all` or `Promise.allSettled`, or keep the list in the
|
|
108
|
+
call's entity. Treat an iterator or an `AbortController` instance created for the call as that
|
|
109
|
+
entity for its own protocol, its cursor, its abort, and its first reason, and never treat an array,
|
|
110
|
+
a record, a `Map`, or a `Set` as that entity. Treat an entity created for one call, or for one unit
|
|
111
|
+
under the **Bound values** rule, as short-lived for that call or unit, and every other entity as
|
|
112
|
+
long-lived. Judge lifetime against the call or bound unit whose state you classify: treat an owner
|
|
113
|
+
that can serve another such call or unit as long-lived, even when that owner belongs to an enclosing
|
|
114
|
+
call or unit.
|
|
115
|
+
- **Entity state.** Keep state that outlives one call in a `#` field on its owning class, never in
|
|
116
|
+
an array, a record, a `Map`, or a `Set` that a function holds in its closure and changes after the
|
|
117
|
+
call that created the function returns. Keep a composable's reactive state as
|
|
118
|
+
`.claude/rules/browser.md` states, and a middleware's changing state where
|
|
119
|
+
`.claude/rules/architecture.md` § Middleware places it.
|
|
120
|
+
- **Call slots.** In a long-lived entity, never keep a call's own entity, context, promise, or
|
|
121
|
+
scratch value in a `#` field that the call sets when it starts or clears when it settles, for its
|
|
122
|
+
steps, its callbacks, a handler registered outside it, or a later call to reach while it runs:
|
|
123
|
+
treat that field as a reassigned binding, even when a refusal keeps the calls from overlapping.
|
|
124
|
+
Reach the call's entity through the **Registries** rule. Treat neither a bound value nor a latch,
|
|
125
|
+
under the **Bound values** rule, as a call slot.
|
|
126
|
+
- **Unit resets.** In a long-lived entity, never let a method that binds or releases a unit of work
|
|
127
|
+
reset a `#` field other than the bound value's own field for the unit's later calls to change,
|
|
128
|
+
whether by assigning the field or by emptying the collection it holds: treat that field as a
|
|
129
|
+
reassigned binding. Treat an assignment as such a reset when it discards the preceding unit's state
|
|
130
|
+
and initializes state for another unit, even when its expression reads the old field. Keep that
|
|
131
|
+
state in `#` fields of the bound value.
|
|
132
|
+
- **Bound values.** When a long-lived entity binds one unit of work at a time that later calls
|
|
133
|
+
continue, such as a goal, a session, or a lease, and holds that unit nowhere else, keep the unit's
|
|
134
|
+
value in one `#` field typed `T | undefined`, never in a collection:
|
|
135
|
+
- in the binding method, throw while the field holds a value and publish the incoming value, both
|
|
136
|
+
before the method's first suspension, and keep asynchronous initialization inside that value's
|
|
137
|
+
lifecycle;
|
|
138
|
+
- in a `start` method that restarts instead, call the bound value's own end method and publish the
|
|
139
|
+
incoming value before the method's first suspension, and await the previous value's end only
|
|
140
|
+
after publishing;
|
|
141
|
+
- set the field back to `undefined` in a later call, such as a `clear` method, the method that ends
|
|
142
|
+
the unit, or the entity's teardown, and never in the binding call's own settle path. Treat as a
|
|
143
|
+
later call only an independent invocation made after the binding invocation returned, never
|
|
144
|
+
cleanup that the binding invocation invokes or registers;
|
|
145
|
+
- narrow the field's `T | undefined` type by throwing, branching, or optional chaining before you
|
|
146
|
+
use the value as present, or return or forward it unnarrowed through a contract that admits
|
|
147
|
+
absence;
|
|
148
|
+
- keep state whose lifetime is the bound unit in `#` fields of the bound value, and keep state that
|
|
149
|
+
continues across units in the owning entity's `#` fields.
|
|
150
|
+
|
|
151
|
+
Treat an explicit bind-to-release lifecycle as a bound unit. Keep several units bound at once in a
|
|
152
|
+
registry under the **Registries** rule, read by key. Treat a field that selects an entry the entity
|
|
153
|
+
already holds, such as the id of its active conversation, as a selection, not a bound value. Treat a
|
|
154
|
+
field as a latch, not a bound value, when it can acquire a value only once during its owner's
|
|
155
|
+
lifetime, whether a repeated invocation throws or keeps that value.
|
|
156
|
+
|
|
157
|
+
- **Registries.** Reach the entities of operations in flight, such as calls, streams, tickets, or
|
|
158
|
+
units bound at once, through a `readonly` `#` registry, a `Set` or a `Map`: add each entity when its
|
|
159
|
+
operation starts, delete it on every path that ends the operation, and clear the registry in bulk
|
|
160
|
+
only in the method that tears the entity down. Delete each settling operation's entry even when
|
|
161
|
+
that leaves the registry empty. Apply this rule to unfinished calls, streams, tickets, and
|
|
162
|
+
explicitly bound units, and never to a manager's population, a subscription, a cache, or a
|
|
163
|
+
persisted record merely because a collection holds it. Read a registry through its `size` property,
|
|
164
|
+
through every entry, or through the entry a key names. Take its entry by position, as
|
|
165
|
+
`const [entry] = registry` does, only while a refusal on every add caps the registry at one entry
|
|
166
|
+
and the call that added the entry deletes it on every path that settles that call, and handle the
|
|
167
|
+
empty registry at that read.
|
|
168
|
+
- **Immutability exceptions.** As an exception to § Immutability, change in place the membership of a
|
|
169
|
+
registry that the **Registries** rule admits, and keep its field binding `readonly`. As an exception
|
|
170
|
+
to the § Immutability ban on mutating caller-owned inputs, let a helper that takes part in one
|
|
171
|
+
owning operation append to that operation's log under the **Lists and logs** rule, or insert into
|
|
172
|
+
its memo under the **Exemptions** rule, and mutate no other caller-owned input.
|
|
173
|
+
|
|
44
174
|
## Errors and outcomes
|
|
45
175
|
|
|
46
176
|
| Condition | Required strategy |
|