@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
|
@@ -21,7 +21,7 @@ Inventory local code involving:
|
|
|
21
21
|
- JSON parsing and narrowing;
|
|
22
22
|
- `Result`, success/failure outcomes, and safe exception capture;
|
|
23
23
|
- contract shapes, compiled guards/parsers/schemas/generators/reporters;
|
|
24
|
-
- finite/integer checks, error narrowing, and
|
|
24
|
+
- finite/integer checks, error narrowing, and untrusted-boundary containment.
|
|
25
25
|
|
|
26
26
|
For each local symbol, record:
|
|
27
27
|
|
|
@@ -46,7 +46,7 @@ Do not force contract abstractions into plain control flow where they obscure be
|
|
|
46
46
|
|
|
47
47
|
Test every adopted boundary for relevant invariants:
|
|
48
48
|
|
|
49
|
-
- guards are total for
|
|
49
|
+
- guards are total for any value, including revoked proxies, throwing accessors, and exotic prototypes;
|
|
50
50
|
- parsed values satisfy the paired guard;
|
|
51
51
|
- guard-valid values retain identity when promised;
|
|
52
52
|
- invalid or ambiguous values fail in the documented form;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Equivalence of a rewrite
|
|
2
|
+
|
|
3
|
+
Judge a structural rewrite that claims unchanged behavior, such as a sweep under
|
|
4
|
+
`.claude/rules/typescript.md` § Bindings, a centralization move, or a nested-function extraction, by
|
|
5
|
+
this contract against its baseline revision. Prove it with a differential under
|
|
6
|
+
`.claude/rules/quality.md` § Instruments that runs the baseline and the rewrite on the same inputs,
|
|
7
|
+
with a row for each class this contract names that the changed code admits.
|
|
8
|
+
|
|
9
|
+
## Hold the contract
|
|
10
|
+
|
|
11
|
+
- Treat every value the declared parameter types admit as inside the contract: holes, entries read
|
|
12
|
+
through getters (their count and order), entries that throw, an array whose `Symbol.iterator` is
|
|
13
|
+
missing or replaced, and an iterator's `return()`.
|
|
14
|
+
- Match the outcome (the return value, or the thrown error's constructor), the element reads (which
|
|
15
|
+
indices, in what order, how often), and every effect before a throw. Let a message differ only
|
|
16
|
+
where the throwing expression differs, and name each such row.
|
|
17
|
+
- For an entry whose own getters are pure, returning the same value on every read with no side
|
|
18
|
+
effect, match the outcome. For an entry that throws, match the thrown constructor. Leave the count
|
|
19
|
+
and order of an entry's own property reads outside the contract.
|
|
20
|
+
- Leave outside the contract a value outside the declared types, such as a number or a plain
|
|
21
|
+
array-like where an array is declared; a `Proxy` trap other than element `get` and
|
|
22
|
+
`Symbol.iterator`; the order of a record's own property reads when each is read as often as before
|
|
23
|
+
and no outcome changes; and tampering with a global prototype.
|
|
24
|
+
|
|
25
|
+
## Keep the iterator protocol
|
|
26
|
+
|
|
27
|
+
When a rewrite replaces a `for…of` over a caller's array, refuse each form that changes what it
|
|
28
|
+
reads:
|
|
29
|
+
|
|
30
|
+
- `Array.from(source, mapper)` falls back to index access when `Symbol.iterator` is missing, so it
|
|
31
|
+
returns where `for…of` threw a `TypeError`.
|
|
32
|
+
- `source.values()` and the array methods read by index, so they accept a missing iterator and
|
|
33
|
+
bypass a replaced or closing one; `flatMap`, `filter`, `reduce`, `forEach`, `some`, and `every`
|
|
34
|
+
skip a hole that `for…of` reads.
|
|
35
|
+
- A spread copies every element first, so it reads past a hole or ahead of a throwing entry where
|
|
36
|
+
the baseline threw at the earlier element.
|
|
37
|
+
|
|
38
|
+
Consume the source through `Iterator.from(source)` and its helpers, or through a generator whose
|
|
39
|
+
body is the baseline `for…of`. To keep `Array.from`, hand it an iterable that forwards only
|
|
40
|
+
`[Symbol.iterator]()`. Never bound a hand-written stepper by `length`: a replaced iterator that
|
|
41
|
+
yields more entries than `length` is truncated.
|
|
42
|
+
|
|
43
|
+
## Keep every test
|
|
44
|
+
|
|
45
|
+
Before a claim that a rewrite lost no test, run
|
|
46
|
+
`node .agents/skills/orkestrel-harden/scripts/titles.ts --base REV`, where `REV` is the baseline
|
|
47
|
+
revision. Restore each missing title, or rule its removal with the behavior it proved, and confirm
|
|
48
|
+
each moved title by reading the registration that uses it.
|
|
@@ -10,7 +10,7 @@ List every boundary where assumptions can fail:
|
|
|
10
10
|
- partial work, abort, timeout, retry, pause/resume, stop/destroy, and cleanup;
|
|
11
11
|
- concurrency, reentrancy, ordering, idempotency, fairness, and backpressure;
|
|
12
12
|
- filesystem, process, socket, protocol, browser, model, and network boundaries;
|
|
13
|
-
-
|
|
13
|
+
- malformed or unexpected strings, paths, headers, frames, JSON, prototypes, getters, and proxies;
|
|
14
14
|
- resource ownership, leaks, teardown, and pressure;
|
|
15
15
|
- environment isolation and unsupported-host behavior;
|
|
16
16
|
- serialization, restore, public exports, and consumer ergonomics.
|
|
@@ -78,7 +78,7 @@ For every fetch, write, delete, extraction, path, protocol, or authentication bo
|
|
|
78
78
|
- fail closed when an allowlist or target cannot be established;
|
|
79
79
|
- avoid logging secrets or untrusted full payloads;
|
|
80
80
|
- make destructive scope explicit and recoverable where practical;
|
|
81
|
-
- prove malformed, partial, oversized, and
|
|
81
|
+
- prove malformed, partial, oversized, and unexpected input behavior.
|
|
82
82
|
|
|
83
83
|
## Audit the tests themselves
|
|
84
84
|
|
|
@@ -92,6 +92,8 @@ Run `node .agents/skills/orkestrel-harden/scripts/discovery.ts` for the discover
|
|
|
92
92
|
- helpers do not reimplement production logic;
|
|
93
93
|
- real cleanup runs even after assertion or setup failure.
|
|
94
94
|
|
|
95
|
+
Collection evaluates every collected project's `setupFiles`, so the census runs each setup module's load-time work, and `--projects` narrows the report, never that listing. When a setup module contacts a live service at load, such as a `service` project's readiness gate in `tests/setupService.ts`, run the census only while an instance the run may contact is up and the service's address points at it; with the address on a closed port, that project's listing fails and the census exits 2 naming its gate.
|
|
96
|
+
|
|
95
97
|
## Inspect generated outputs
|
|
96
98
|
|
|
97
99
|
- build all supported environments;
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Run from the checkout root:
|
|
2
|
+
// node .agents/skills/orkestrel-harden/scripts/titles.ts --base REV [--root DIR] [--json] [--out FILE]
|
|
3
|
+
// Compare exact decoded describe/it/test title counts in tests/**/*.test.ts against REV. Report
|
|
4
|
+
// deficits with their baseline files, or as moved when another tests/**/*.ts string literal holds
|
|
5
|
+
// the title. Confirm moved registrations by reading their holder. Decode quote and backslash
|
|
6
|
+
// escapes only. Balance each-table parentheses over code tokens, skipping lexical regions.
|
|
7
|
+
// Print text by default or one JSON line with --json; --out writes .json or .txt independently.
|
|
8
|
+
// Compare interpolated templates by full raw spelling. Refuse unclosed lexical tokens.
|
|
9
|
+
// Track lexical brace context without a syntax tree. Function-expression and arrow bodies cannot
|
|
10
|
+
// establish a following slash's grammar; refuse that detectable ambiguity instead of claiming a move.
|
|
11
|
+
// Exit 0 with no missing titles, 3 with missing titles, 2 on git, filesystem or token refusal, 64 on usage.
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { dirname, extname, join, relative, resolve } from 'node:path'
|
|
15
|
+
import {
|
|
16
|
+
listFiles,
|
|
17
|
+
normalizeSlashes,
|
|
18
|
+
readMissingFlags,
|
|
19
|
+
readOption,
|
|
20
|
+
} from '../../orkestrel-dispatch/scripts/helpers.ts'
|
|
21
|
+
|
|
22
|
+
interface Title {
|
|
23
|
+
readonly file: string
|
|
24
|
+
readonly title: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Missing extends Title {
|
|
28
|
+
readonly deficit: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Moved extends Title {
|
|
32
|
+
readonly holder: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Report {
|
|
36
|
+
readonly base: string
|
|
37
|
+
readonly root: string
|
|
38
|
+
readonly count: number
|
|
39
|
+
readonly missing: readonly Missing[]
|
|
40
|
+
readonly moved: readonly Moved[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Token {
|
|
44
|
+
readonly category: 'code' | 'line' | 'block' | 'string' | 'template' | 'regex'
|
|
45
|
+
readonly start: number
|
|
46
|
+
readonly end: number
|
|
47
|
+
readonly children: readonly Token[]
|
|
48
|
+
readonly prefix?: boolean
|
|
49
|
+
readonly ambiguous?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Brace {
|
|
53
|
+
readonly category: 'block' | 'expression' | 'template'
|
|
54
|
+
readonly ambiguous: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Parenthesis {
|
|
58
|
+
readonly prefix: boolean
|
|
59
|
+
readonly ambiguous: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const TITLE_PATTERN = /^\s*(?:it|test|describe)(?:\.\w+)*\(/gmu
|
|
63
|
+
const REGEX_PREFIX =
|
|
64
|
+
/^(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await|else|do)$/u
|
|
65
|
+
|
|
66
|
+
function decodeTitle(text: string, token: Token): string {
|
|
67
|
+
const raw = text.slice(token.start + 1, token.end - 1)
|
|
68
|
+
return token.category === 'template' && token.children.length > 0
|
|
69
|
+
? raw
|
|
70
|
+
: raw.replace(/\\(['"`\\])/gu, '$1')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class TokenScanner {
|
|
74
|
+
readonly #text: string
|
|
75
|
+
readonly #file: string
|
|
76
|
+
readonly #parentheses: Parenthesis[] = []
|
|
77
|
+
readonly #braces: Brace[] = []
|
|
78
|
+
#position = 0
|
|
79
|
+
|
|
80
|
+
constructor(text: string, file: string) {
|
|
81
|
+
this.#text = text
|
|
82
|
+
this.#file = file
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
scan(): readonly Token[] {
|
|
86
|
+
return this.#readCode(false, 0)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#readCode(expression: boolean, depth: number): readonly Token[] {
|
|
90
|
+
if (depth > 256) throw new Error(`${this.#file}: token nesting exceeds 256`)
|
|
91
|
+
const tokens: Token[] = []
|
|
92
|
+
while (this.#position < this.#text.length) {
|
|
93
|
+
const start = this.#position
|
|
94
|
+
const rest = this.#text.slice(start)
|
|
95
|
+
const character = this.#text[start]
|
|
96
|
+
const previous = tokens.findLast(
|
|
97
|
+
(token) =>
|
|
98
|
+
token.category !== 'line' &&
|
|
99
|
+
token.category !== 'block' &&
|
|
100
|
+
this.#text.slice(token.start, token.end).trim().length > 0,
|
|
101
|
+
)
|
|
102
|
+
const spelling = previous === undefined ? '' : this.#text.slice(previous.start, previous.end)
|
|
103
|
+
if (character === '}' && expression) {
|
|
104
|
+
this.#position += 1
|
|
105
|
+
return tokens
|
|
106
|
+
}
|
|
107
|
+
if (character === '{') {
|
|
108
|
+
const block =
|
|
109
|
+
previous === undefined
|
|
110
|
+
? this.#braces.at(-1)?.category !== 'template'
|
|
111
|
+
: previous.category === 'code' &&
|
|
112
|
+
/^(?:\)|=>|;|\{|\}|else|try|finally|do)$/u.test(spelling)
|
|
113
|
+
this.#braces.push({
|
|
114
|
+
category: block ? 'block' : 'expression',
|
|
115
|
+
ambiguous: block && (spelling === '=>' || previous?.ambiguous === true),
|
|
116
|
+
})
|
|
117
|
+
this.#position += 1
|
|
118
|
+
tokens.push({ category: 'code', start, end: this.#position, children: [] })
|
|
119
|
+
tokens.push(...this.#readCode(true, depth + 1))
|
|
120
|
+
const brace = this.#braces.pop()
|
|
121
|
+
tokens.push({
|
|
122
|
+
category: 'code',
|
|
123
|
+
start: this.#position - 1,
|
|
124
|
+
end: this.#position,
|
|
125
|
+
children: [],
|
|
126
|
+
prefix: brace?.category === 'block',
|
|
127
|
+
ambiguous: brace?.ambiguous === true,
|
|
128
|
+
})
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (character === '`') {
|
|
132
|
+
tokens.push(this.#readTemplate(depth + 1))
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
character === '/' &&
|
|
137
|
+
!rest.startsWith('//') &&
|
|
138
|
+
!rest.startsWith('/*') &&
|
|
139
|
+
previous?.ambiguous === true
|
|
140
|
+
)
|
|
141
|
+
throw new Error(`${this.#file}:${start}: Ambiguous brace before slash`)
|
|
142
|
+
const regex =
|
|
143
|
+
character === '/' &&
|
|
144
|
+
!rest.startsWith('//') &&
|
|
145
|
+
!rest.startsWith('/*') &&
|
|
146
|
+
(previous === undefined ||
|
|
147
|
+
previous.prefix === true ||
|
|
148
|
+
(previous.category === 'code' &&
|
|
149
|
+
(REGEX_PREFIX.test(spelling) ||
|
|
150
|
+
!/^(?:[\p{ID_Continue}$]+|[\])}]|\+\+|--)$/u.test(spelling))))
|
|
151
|
+
const category = rest.startsWith('//')
|
|
152
|
+
? 'line'
|
|
153
|
+
: rest.startsWith('/*')
|
|
154
|
+
? 'block'
|
|
155
|
+
: character === "'" || character === '"'
|
|
156
|
+
? 'string'
|
|
157
|
+
: regex
|
|
158
|
+
? 'regex'
|
|
159
|
+
: 'code'
|
|
160
|
+
const match =
|
|
161
|
+
category === 'line'
|
|
162
|
+
? /^\/\/[^\r\n]*/u.exec(rest)
|
|
163
|
+
: category === 'block'
|
|
164
|
+
? /^\/\*[\s\S]*?\*\//u.exec(rest)
|
|
165
|
+
: category === 'string'
|
|
166
|
+
? (character === "'"
|
|
167
|
+
? /^'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n\u2028\u2029])*'/u
|
|
168
|
+
: /^"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n\u2028\u2029])*"/u
|
|
169
|
+
).exec(rest)
|
|
170
|
+
: category === 'regex'
|
|
171
|
+
? /^\/(?:\\[^\r\n]|\[(?:\\[^\r\n]|[^\]\\\r\n])*\]|[^/\\[\r\n])*\/[a-z]*/iu.exec(
|
|
172
|
+
rest,
|
|
173
|
+
)
|
|
174
|
+
: /^(?:[\p{ID_Start}_$][\p{ID_Continue}$]*|\d[\w.]*|\s+|\+\+|--|=>|.)/u.exec(rest)
|
|
175
|
+
if (match === null) throw new Error(`${this.#file}:${start}: Unclosed ${category} token`)
|
|
176
|
+
this.#position += match[0].length
|
|
177
|
+
if (category === 'code' && character === '(') {
|
|
178
|
+
const words = tokens
|
|
179
|
+
.filter((token) => token.category === 'code')
|
|
180
|
+
.map((token) => this.#text.slice(token.start, token.end))
|
|
181
|
+
.filter((word) => word.trim().length > 0)
|
|
182
|
+
const keyword = words.findLastIndex((word) => word === 'function')
|
|
183
|
+
const callable =
|
|
184
|
+
keyword >= 0 &&
|
|
185
|
+
words
|
|
186
|
+
.slice(keyword + 1)
|
|
187
|
+
.every((word) => word === '*' || /^[\p{ID_Start}_$][\p{ID_Continue}$]*$/u.test(word))
|
|
188
|
+
const before = words[words[keyword - 1] === 'async' ? keyword - 2 : keyword - 1]
|
|
189
|
+
this.#parentheses.push({
|
|
190
|
+
prefix: /^(?:if|while|for|with|switch|catch)$/u.test(spelling),
|
|
191
|
+
ambiguous:
|
|
192
|
+
callable &&
|
|
193
|
+
(before === undefined
|
|
194
|
+
? this.#braces.at(-1)?.category === 'template'
|
|
195
|
+
: /^(?:[=(,:[?+\-*/%&|^!~<>]|=>|return|throw|yield|await|case|new|void|typeof|delete|instanceof|in|of)$/u.test(
|
|
196
|
+
before,
|
|
197
|
+
)),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
const parenthesis =
|
|
201
|
+
category === 'code' && character === ')' ? this.#parentheses.pop() : undefined
|
|
202
|
+
tokens.push({
|
|
203
|
+
category,
|
|
204
|
+
start,
|
|
205
|
+
end: this.#position,
|
|
206
|
+
children: [],
|
|
207
|
+
prefix: parenthesis?.prefix === true,
|
|
208
|
+
ambiguous: parenthesis?.ambiguous === true,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
if (expression)
|
|
212
|
+
throw new Error(`${this.#file}:${this.#position}: Unclosed template expression or brace`)
|
|
213
|
+
return tokens
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#readTemplate(depth: number): Token {
|
|
217
|
+
const start = this.#position
|
|
218
|
+
const children: Token[] = []
|
|
219
|
+
this.#position += 1
|
|
220
|
+
while (this.#position < this.#text.length) {
|
|
221
|
+
if (this.#text[this.#position] === '\\') {
|
|
222
|
+
this.#position += 2
|
|
223
|
+
} else if (this.#text[this.#position] === '`') {
|
|
224
|
+
this.#position += 1
|
|
225
|
+
return { category: 'template', start, end: this.#position, children }
|
|
226
|
+
} else if (this.#text.startsWith('${', this.#position)) {
|
|
227
|
+
const opening = this.#position
|
|
228
|
+
this.#position += 2
|
|
229
|
+
this.#braces.push({ category: 'template', ambiguous: false })
|
|
230
|
+
const expression = this.#readCode(true, depth)
|
|
231
|
+
this.#braces.pop()
|
|
232
|
+
children.push({
|
|
233
|
+
category: 'code',
|
|
234
|
+
start: opening,
|
|
235
|
+
end: this.#position,
|
|
236
|
+
children: expression,
|
|
237
|
+
})
|
|
238
|
+
} else {
|
|
239
|
+
this.#position += 1
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`${this.#file}:${start}: Unclosed template token`)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readArguments(
|
|
247
|
+
text: string,
|
|
248
|
+
tokens: IterableIterator<Token>,
|
|
249
|
+
location: string,
|
|
250
|
+
depth: number,
|
|
251
|
+
): number {
|
|
252
|
+
if (depth > 256) throw new Error(`${location}: each argument nesting exceeds 256`)
|
|
253
|
+
for (const token of tokens) {
|
|
254
|
+
if (token.category !== 'code') continue
|
|
255
|
+
const spelling = text.slice(token.start, token.end)
|
|
256
|
+
if (spelling === '(') readArguments(text, tokens, location, depth + 1)
|
|
257
|
+
else if (spelling === ')') return token.end
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`${location}: Unbalanced each arguments`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readRegistrations(text: string, tokens: readonly Token[], file: string): readonly Token[] {
|
|
263
|
+
const code = tokens
|
|
264
|
+
.map((token) => {
|
|
265
|
+
const raw = text.slice(token.start, token.end)
|
|
266
|
+
if (token.category === 'code') return raw
|
|
267
|
+
const blank = raw
|
|
268
|
+
.split('')
|
|
269
|
+
.map((character) => (/[\r\n]/u.test(character) ? character : ' '))
|
|
270
|
+
.join('')
|
|
271
|
+
return token.category === 'string' || token.category === 'template'
|
|
272
|
+
? `\0${blank.slice(1)}`
|
|
273
|
+
: blank
|
|
274
|
+
})
|
|
275
|
+
.join('')
|
|
276
|
+
const offset = tokens[0]?.start ?? 0
|
|
277
|
+
const positions = new Map(tokens.map((token, index) => [token.start, index]))
|
|
278
|
+
const starts = new Set(
|
|
279
|
+
Array.from(code.matchAll(TITLE_PATTERN)).flatMap((match) => {
|
|
280
|
+
const opening = offset + match.index + match[0].length - 1
|
|
281
|
+
const direct = /^\s*\0/u.exec(code.slice(opening + 1 - offset))
|
|
282
|
+
if (!/\.each\(/u.test(match[0]) && direct !== null) return [opening + direct[0].length]
|
|
283
|
+
const index = positions.get(opening)
|
|
284
|
+
if (index === undefined) return []
|
|
285
|
+
const line = text.slice(0, opening).split(/\r\n|\n/u).length
|
|
286
|
+
const end = readArguments(text, tokens.slice(index + 1).values(), `${file}:${line}`, 0)
|
|
287
|
+
const title = /^\s*\(\s*\0/u.exec(code.slice(end - offset))
|
|
288
|
+
return title === null ? [] : [end + title[0].length - 1]
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
291
|
+
return [
|
|
292
|
+
...tokens.filter((token) => starts.has(token.start)),
|
|
293
|
+
...tokens.flatMap((token) => readRegistrations(text, token.children, file)),
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function collectLiterals(tokens: readonly Token[]): readonly Token[] {
|
|
298
|
+
return tokens.flatMap((token) => [
|
|
299
|
+
...(token.category === 'string' || token.category === 'template' ? [token] : []),
|
|
300
|
+
...collectLiterals(token.children),
|
|
301
|
+
])
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readGit(root: string, args: readonly string[]): string {
|
|
305
|
+
const result = spawnSync('git', ['-C', root, ...args], {
|
|
306
|
+
encoding: 'utf8',
|
|
307
|
+
windowsHide: true,
|
|
308
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
309
|
+
})
|
|
310
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
311
|
+
throw new Error(`git: ${result.error?.message ?? result.stderr.trim()}`)
|
|
312
|
+
}
|
|
313
|
+
return result.stdout
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readReport(base: string, root: string): Report {
|
|
317
|
+
const baseline = readGit(root, ['ls-tree', '-r', '-z', '--name-only', base, '--', 'tests'])
|
|
318
|
+
.split('\0')
|
|
319
|
+
.filter((file) => file.endsWith('.test.ts'))
|
|
320
|
+
.flatMap((file) => {
|
|
321
|
+
const text = readGit(root, ['show', `${base}:${file}`])
|
|
322
|
+
return readRegistrations(
|
|
323
|
+
text,
|
|
324
|
+
new TokenScanner(text, `${base}:${file}`).scan(),
|
|
325
|
+
`${base}:${file}`,
|
|
326
|
+
).map((token) => ({
|
|
327
|
+
file,
|
|
328
|
+
title: decodeTitle(text, token),
|
|
329
|
+
}))
|
|
330
|
+
})
|
|
331
|
+
const directory = join(root, 'tests')
|
|
332
|
+
const files = (existsSync(directory) ? listFiles(directory) : [])
|
|
333
|
+
.filter(({ path }) => path.endsWith('.ts'))
|
|
334
|
+
.map(({ path }) => {
|
|
335
|
+
const file = normalizeSlashes(relative(root, path))
|
|
336
|
+
const text = readFileSync(path, 'utf8')
|
|
337
|
+
const tokens = new TokenScanner(text, file).scan()
|
|
338
|
+
return {
|
|
339
|
+
file,
|
|
340
|
+
text,
|
|
341
|
+
tokens,
|
|
342
|
+
registrations: file.endsWith('.test.ts') ? readRegistrations(text, tokens, file) : [],
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
.sort((left, right) => left.file.localeCompare(right.file))
|
|
346
|
+
const titles = files
|
|
347
|
+
.filter(({ file }) => file.endsWith('.test.ts'))
|
|
348
|
+
.flatMap(({ file, text, registrations }) =>
|
|
349
|
+
registrations.map((token) => ({ file, title: decodeTitle(text, token) })),
|
|
350
|
+
)
|
|
351
|
+
// A surviving registration supplies its title count, never evidence that a lost duplicate moved.
|
|
352
|
+
const holders = files.flatMap(({ file, text, tokens, registrations }) =>
|
|
353
|
+
collectLiterals(tokens)
|
|
354
|
+
.filter((token) => !registrations.includes(token))
|
|
355
|
+
.map((token) => ({ file, title: decodeTitle(text, token) })),
|
|
356
|
+
)
|
|
357
|
+
const deficits = [...Map.groupBy(baseline, ({ title }) => title)].flatMap(([title, entries]) => {
|
|
358
|
+
const survivors = titles.filter((candidate) => candidate.title === title)
|
|
359
|
+
const unmatched = [...Map.groupBy(entries, ({ file }) => file)].flatMap(([file, local]) =>
|
|
360
|
+
local.slice(survivors.filter((candidate) => candidate.file === file).length),
|
|
361
|
+
)
|
|
362
|
+
const surplus = [...Map.groupBy(survivors, ({ file }) => file)].flatMap(([file, local]) =>
|
|
363
|
+
local.slice(entries.filter((candidate) => candidate.file === file).length),
|
|
364
|
+
)
|
|
365
|
+
return [...Map.groupBy(unmatched.slice(surplus.length), ({ file }) => file)].map(
|
|
366
|
+
([file, lost]) => ({
|
|
367
|
+
file,
|
|
368
|
+
title,
|
|
369
|
+
deficit: lost.length,
|
|
370
|
+
}),
|
|
371
|
+
)
|
|
372
|
+
})
|
|
373
|
+
const classified = deficits.map((entry) => ({
|
|
374
|
+
entry,
|
|
375
|
+
holder: holders.find(({ title }) => title === entry.title)?.file,
|
|
376
|
+
}))
|
|
377
|
+
return {
|
|
378
|
+
base,
|
|
379
|
+
root: normalizeSlashes(root),
|
|
380
|
+
count: baseline.length,
|
|
381
|
+
missing: classified.flatMap(({ entry, holder }) => (holder === undefined ? [entry] : [])),
|
|
382
|
+
moved: classified.flatMap(({ entry, holder }) =>
|
|
383
|
+
holder === undefined ? [] : [{ file: entry.file, title: entry.title, holder }],
|
|
384
|
+
),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function renderReport(report: Report): string {
|
|
389
|
+
return [
|
|
390
|
+
`titles: ${report.count} baseline title(s) at ${report.base}, ${report.missing.length} missing, ${report.moved.length} moved`,
|
|
391
|
+
...report.missing.map(({ file, title, deficit }) => `missing\t${file}\t${title}\t${deficit}`),
|
|
392
|
+
...report.moved.map(({ file, title, holder }) => `moved\t${file}\t${title}\t${holder}`),
|
|
393
|
+
].join('\n')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function main(argv: readonly string[]): number {
|
|
397
|
+
const missing = readMissingFlags(argv, ['--base', '--root', '--out'])
|
|
398
|
+
if (missing.length > 0) {
|
|
399
|
+
console.error(`titles: ${missing.join(', ')} given with no value`)
|
|
400
|
+
return 64
|
|
401
|
+
}
|
|
402
|
+
const base = readOption(argv, '--base')
|
|
403
|
+
const out = readOption(argv, '--out')
|
|
404
|
+
if (base === undefined || (out !== undefined && !['.json', '.txt'].includes(extname(out)))) {
|
|
405
|
+
console.error('usage: titles.ts --base REV [--root DIR] [--json] [--out FILE.json|FILE.txt]')
|
|
406
|
+
return 64
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const report = readReport(base, resolve(readOption(argv, '--root') ?? process.cwd()))
|
|
410
|
+
const json = JSON.stringify(report)
|
|
411
|
+
const text = renderReport(report)
|
|
412
|
+
if (out !== undefined) {
|
|
413
|
+
const target = resolve(out)
|
|
414
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
415
|
+
writeFileSync(target, `${extname(target) === '.json' ? json : text}\n`)
|
|
416
|
+
}
|
|
417
|
+
console.log(argv.includes('--json') ? json : text)
|
|
418
|
+
return report.missing.length > 0 ? 3 : 0
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(`titles: ${error instanceof Error ? error.message : String(error)}`)
|
|
421
|
+
return 2
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
process.exitCode = main(process.argv.slice(2))
|
|
@@ -201,8 +201,8 @@ await PORTFOLIO.place('home')
|
|
|
201
201
|
control, a Tab away, or a named button. Report a screen that commits on a timer, on an
|
|
202
202
|
unpredictable event, or only after work the person cannot observe as a screen finding, and never
|
|
203
203
|
work around it in the layer.
|
|
204
|
-
7. **Type only what a person would.** Journeys carry trusted input;
|
|
205
|
-
the transport family and the parser suites.
|
|
204
|
+
7. **Type only what a person would.** Journeys carry trusted input; inputs no person would type
|
|
205
|
+
belong to the transport family and the parser suites.
|
|
206
206
|
8. **Perform every interaction step unconditionally.** Never gate a step on whether the control it
|
|
207
207
|
is about to drive exists or is reachable, and never branch a journey on `readRefusal`. Let the
|
|
208
208
|
resolver's failure voice name what the interface withheld. A guarded step passes whether or not
|
|
@@ -30,7 +30,8 @@ file is written to, and the flag as `enabled`.
|
|
|
30
30
|
picture is one element.
|
|
31
31
|
- Take `enabled` from `inject('capture')`, which the generated root configuration provides from the
|
|
32
32
|
`CAPTURE` environment variable set to `1` in the shell that runs the axis. An ordinary run leaves
|
|
33
|
-
it false, so `place` returns `undefined`, resizes nothing, writes nothing,
|
|
33
|
+
it false, so `place` returns `undefined`, resizes nothing, writes nothing, records no placement,
|
|
34
|
+
and still refuses an unregistered state and a second placement of one state.
|
|
34
35
|
- Read `files` for the registry expanded across every variant, `placements` for what this run placed,
|
|
35
36
|
and `paths` for what it wrote. Each hands back a snapshot.
|
|
36
37
|
- Reach for `readFrame` where the written image itself is the subject — its decoded size, or its
|
|
@@ -42,8 +43,8 @@ The package already refuses these, so assert none of them again:
|
|
|
42
43
|
| Refusal | Raised by |
|
|
43
44
|
| -------------------------------------------------------------------- | --------------------- |
|
|
44
45
|
| `Capture variant "<name>" is not registered` | `createPortfolio` |
|
|
45
|
-
| `Capture state "<state>" is not registered` | `place`,
|
|
46
|
-
| `Capture state "<state>" is already placed` | `place`,
|
|
46
|
+
| `Capture state "<state>" is not registered` | `place`, in every run |
|
|
47
|
+
| `Capture state "<state>" is already placed` | `place`, in every run |
|
|
47
48
|
| `Capture frame at <path> is not the one this run shot` | `captureFrame` |
|
|
48
49
|
| `Capture frame was written to <written> where <asked> was asked for` | `captureFrame` |
|
|
49
50
|
|
|
@@ -92,11 +93,11 @@ Write each of the following proofs into the suite. The package asserts none of t
|
|
|
92
93
|
- Keep the filename proof always-on, so a registry edit that introduces a duplicate or a collision
|
|
93
94
|
fails the ordinary run. Compare `expandCaptures(states, variants)` against `files` and against a
|
|
94
95
|
set built from it; a length equal to the set's size is what proves the expansion unique.
|
|
95
|
-
- Assert placement as set equality against the registry, in every run.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
- Assert placement as set equality against the registry, in every run.
|
|
97
|
+
- Read the placement proof from the suite's own set rather than from `placements`, which stays empty
|
|
98
|
+
in an ordinary run. `place` refuses an unregistered name and a second placement of one state in
|
|
99
|
+
every run, but a registered state that no journey placed reaches no refusal; the always-on
|
|
100
|
+
placement proof catches it.
|
|
100
101
|
- Under the flag, assert the written filenames equal the registry expanded for the run's variant.
|
|
101
102
|
`captureFrame` already reads each file back and compares its bytes against the frame it shot, so
|
|
102
103
|
the path `place` returns is proof the file exists and holds that frame.
|
|
@@ -55,8 +55,8 @@ over the same surface needs an instruction from the user.
|
|
|
55
55
|
subagent with a clean context, blind to the others until reconciliation, run once, and no
|
|
56
56
|
lane is dropped because an engine is dark. A narrow verdict request runs the selected lane once
|
|
57
57
|
and stops.
|
|
58
|
-
4. **Reconcile.** Confirmed findings get carriers; refuted findings
|
|
59
|
-
the evidence that
|
|
58
|
+
4. **Reconcile.** Confirmed findings get carriers; refuted findings close on the record with
|
|
59
|
+
the evidence that refuted them; conflicts get a dated ruling. Then walk the reconciled
|
|
60
60
|
list once and name the fix-brief item carrying each finding — a finding with no carrier
|
|
61
61
|
is a dropped finding, and it will cost the next round.
|
|
62
62
|
5. **Dispatch serialized fix units,** objective lane first because it changes the ground
|
|
@@ -82,7 +82,7 @@ over the same surface needs an instruction from the user.
|
|
|
82
82
|
- Do not narrow a selector, add a local exception, or restyle one instance to make a
|
|
83
83
|
capture pass while the underlying rule stays wrong.
|
|
84
84
|
- When a law of the repository genuinely conflicts with the surface's required anatomy,
|
|
85
|
-
stop and report both horns with evidence. An improvised local exception
|
|
85
|
+
stop and report both horns with evidence. An improvised local exception voids an
|
|
86
86
|
enforced invariant.
|
|
87
87
|
|
|
88
88
|
## Return the fixed verdict shape
|
|
@@ -25,7 +25,7 @@ description: Run an Orkestrel release from layer order to registry confirmation.
|
|
|
25
25
|
| `scripts/compare.ts` | `[--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]`, run from the package root after its build: fetches the published tarball and lists the material differences from the rebuilt `dist/` (sourcemaps excluded, whitespace-only differences ignored). Exit 0 when nothing differs, 3 when something does, 2 when the tarball cannot be read, 64 on usage (no package name, no `dist/`, or `--tarball` with no path). |
|
|
26
26
|
| `scripts/pins.ts` | `--version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]`, run from the package root: lists every line under the paths carrying the prior version literal or a prior range literal, once per line, sweeping a one-comparator range such as `^8.3.0` for its bare version in any form. Exit 0 with no hits, 3 with hits, 64 on usage (no literal, or a flag with no value). |
|
|
27
27
|
| `scripts/swap.ts` | One mode per run, from any directory: `--pin --consumer DIR --source DIR [--source DIR ...] [--override NAME ...] [--no-build] [--dry-run] [--json]` refuses a source whose packed manifest names a local dependency spec in any dependency field (a `file:` or `link:` prefix, a path start of `.`, `~/`, `/`, `\`, or a drive letter, or a protocol-free archive name ending in `.tgz`, `.tar.gz`, or `.tar` that is not a `user/repo` shorthand), builds and packs each source into `<consumer>/tmp/tarballs/` under a name carrying its SHA-256, records each replaced range in the ledger `pins.json` there, the record of which build the consumer runs, writes each tarball's absolute `file:` spec and each `--override` (repeatable, like `--source`) where oxfmt sorts it, installs, and proves that every pinned source, an override-only one included, is installed with the source's bytes; `--restore --consumer DIR [--package NAME ...] [--json]` puts back each recorded range and override, keeps an `overrides` block that existed before the pin, installs, proves the restored copy, and lists the re-pin arguments, and refuses with 64, before it writes anything, a ledger whose package or override keys are not package names or whose removal would leave `node_modules`; `--status --consumer DIR [--json]` writes nothing and reports drift between the ledger, the manifest, the tarballs, the installed bytes, and the source commits, a manifest spec holding its tarball only when it names the ledger archive's location. `--pin` and `--restore` write a receipt under `tmp/units/`. Exit 0 on success, 1 when a command or a proof failed, 3 when `--status` reports drift, 64 on usage. |
|
|
28
|
-
| `scripts/wave.ts` | `--visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]` runs one repository's visit in the order [wave.md](references/wave.md) § Visit a repository fixes (pin, commit, overwrite, verify, install, pins, format, gates, compare), stops at the first failing step, and prints each step's exit (a failed step names the `code: message` refusal or the `note` a JSON verb writes on stdout, else the last stderr lines), the bump ruling (the rebuilt dist against the published tarball, and the final runtime dependency set against the published manifest), and a row for `.orkestrel/release.md`; `--prior` names the version a pre-ruled bump replaced so the self-pin sweep finds it; `--plan` prints the packages by layer from the catalog table. Exit 0 when every step passed, 1 when one failed, 2 when a reading failed, 64 on usage
|
|
28
|
+
| `scripts/wave.ts` | `--visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]` runs one repository's visit in the order [wave.md](references/wave.md) § Visit a repository fixes (pin, commit, overwrite, verify, install, pins, format, record, gates, compare), stops at the first failing step, journals each executed step's full output under the target's `tmp/units/wave/` when the work tree ignores `tmp/` and names the journal in its row, and prints each step's exit (a failed step names the `code: message` refusal or the `note` a JSON verb writes on stdout, else the last stderr lines), the bump ruling (the rebuilt dist against the published tarball, and the final runtime dependency set against the published manifest), and a row for `.orkestrel/release.md`; `--prior` names the version a pre-ruled bump replaced so the self-pin sweep finds it; `--plan` prints the packages by layer from the catalog table. An install that fails `ERESOLVE` names the rerun command, and `--regenerate` replaces the lockfile and `node_modules` from the manifest under the refusals and proof [wave.md](references/wave.md) § Regenerate a lock the install cannot move states; `record` commits the overwrite's writes as `Overwrite with scaffold VERSION`. Exit 0 when every step passed, 1 when one failed, 2 when a reading failed, 64 on usage, including `--regenerate` with `--plan`. |
|
|
29
29
|
| `scripts/window.ts` | `--whoami [--wait SECONDS]` reads or polls `npm whoami`; `--login` prints the operator's login command and polls `whoami` until it answers; `--publish DIR... --otp CODE` refuses a batch whose nearest `.git` entry is a file, including ancestor entries, with exit 3 before contacting npm; it names a linked worktree's primary clone or identifies a submodule, then uploads back to back with `npm publish --ignore-scripts --otp`, journals each upload under `tmp/units/`, stops at the first refusal naming where to resume, and confirms each accepted version against the registry; `--confirm NAME@VERSION...` re-reads the registry until it serves the version. Exit 0 on success, 3 when not. |
|
|
30
30
|
|
|
31
31
|
The user's current instruction wins. `references/release.md` owns the credential
|
|
@@ -53,7 +53,7 @@ following the skill.
|
|
|
53
53
|
Derive each pin from that reading, never from a local manifest.
|
|
54
54
|
3. **Visit each repository.** Run `node .agents/skills/orkestrel-publish/scripts/wave.ts --visit --target <repository>`,
|
|
55
55
|
which runs the visit in [wave.md](references/wave.md) in its stated order and stops at the first
|
|
56
|
-
failing step; repair that step by hand and re-run from it with `--from
|
|
56
|
+
failing step; repair that step by hand and re-run from it with `--from`; for an `ERESOLVE`, follow [wave.md](references/wave.md) § Regenerate a lock the install cannot move. Run visits in parallel
|
|
57
57
|
slices of disjoint repositories, each slice serial inside itself.
|
|
58
58
|
4. **Rule on each package's bump.** Take the visit's comparison and range reading; repeat `compare.ts` only after the artifact changes or an unanswered result. Apply the triggers in [wave.md](references/wave.md) § Rule on
|
|
59
59
|
the bump; `references/release.md` § What a bump obliges owns the blast radius.
|
|
@@ -34,6 +34,7 @@ When a consumer needs a dependency's unpublished source, to meet a defect that l
|
|
|
34
34
|
|
|
35
35
|
## The scaffold surface
|
|
36
36
|
|
|
37
|
+
- After a package's first publish, run `scaffold catalog --all` in the scaffold checkout and release scaffold, so the `surface` rule reads the package's guide from that release.
|
|
37
38
|
- `scaffold` is a development dependency of every package, so it publishes on its own and propagates as files, never as a cascade. Each package builds against the published `scaffold`.
|
|
38
39
|
- `scaffold` ships `dist/host`, the vendored file set every target receives through `repair`. Bump and publish `scaffold` when any vendored byte or the set of vendored paths changes.
|
|
39
40
|
- After a vendored-only release, re-pin `@orkestrel/scaffold` in each target, run `repair` there, and prove that target's gates still green. A target bumps only when its own published surface moved.
|