dsh-tower 2.3.0 → 2.4.0
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/CHANGELOG.md +34 -0
- package/COMPATIBILITY.md +1 -0
- package/CONTRIBUTING.md +17 -3
- package/PRINCIPLES.md +18 -0
- package/README.en.md +29 -16
- package/README.md +22 -15
- package/cordis.patch.yml +56 -28
- package/docs/README.md +2 -1
- package/docs/adr/0004-surface-budget.md +20 -0
- package/docs/adr/0005-component-surface.md +130 -0
- package/docs/adr/README.md +1 -0
- package/docs/agents/workflow.md +1 -1
- package/docs/architecture/overview.md +25 -7
- package/docs/guide/hot-reload.md +4 -4
- package/docs/guide/install-upgrade-migration.md +13 -0
- package/docs/operations/security.md +1 -1
- package/docs/reference/component-surface.md +135 -0
- package/docs/reference/config.generated.md +2 -10
- package/docs/reference/config.md +11 -9
- package/docs/reference/doctor.md +14 -9
- package/docs/reference/surface-budget.md +52 -23
- package/docs/reference/tools.generated.md +4 -2
- package/docs/reference/tools.md +6 -3
- package/docs/zh/README.md +1 -1
- package/docs/zh/guide/hot-reload.md +3 -3
- package/docs/zh/guide/install-upgrade-migration.md +10 -0
- package/docs/zh/operations/security.md +1 -1
- package/docs/zh/reference/config.md +9 -9
- package/docs/zh/reference/tools.md +6 -4
- package/domain/components.js +641 -0
- package/domain/config/schema.js +37 -27
- package/domain/contracts/outputs.js +10 -0
- package/domain/errors.js +11 -1
- package/domain/mission.js +12 -1
- package/domain/surface.js +154 -55
- package/host/capability-batch.js +15 -0
- package/host/capability-hmr.js +30 -0
- package/host/capability-import.js +11 -0
- package/host/capability-leases.js +11 -0
- package/host/capability-registry.js +149 -0
- package/host/capability-tests.js +16 -0
- package/host/doctor.js +41 -9
- package/host/hmr.js +2 -4
- package/host/presets.js +51 -9
- package/host/tool-registry.js +150 -34
- package/index.js +32 -20
- package/locale/en.json +31 -11
- package/locale/zh.json +30 -10
- package/package.json +16 -3
- package/scripts/check-components.mjs +235 -0
- package/scripts/component-surface.json +395 -0
- package/scripts/generate-component-surface.mjs +59 -0
- package/scripts/generate-tools-reference.mjs +7 -25
- package/scripts/generate-types.mjs +127 -0
- package/scripts/lib/tool-surface.mjs +85 -0
- package/scripts/mount-smoke/fixtures.mjs +3 -3
- package/scripts/mount-smoke/surface.mjs +96 -27
- package/scripts/mount-smoke.mjs +3 -1
- package/scripts/pack-manifest.txt +17 -0
- package/scripts/surface-baseline.json +35 -4
- package/scripts/surface-baseline.mjs +32 -34
- package/scripts/types-coverage.mjs +184 -0
- package/scripts/types-strict-manifest.txt +10 -0
- package/scripts/types-strict.mjs +141 -0
- package/skills.js +8 -1
- package/tools/index.js +2 -1
- package/tools/reload.js +34 -35
- package/tools/status.js +8 -1
- package/types/index.d.ts +28 -12
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"date": "2026-10-23",
|
|
4
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
5
|
+
"classes": {
|
|
6
|
+
"core": "Removing it breaks the Tower contract. It is never a switch.",
|
|
7
|
+
"optional-component": "A capability whose off state leaves the rest of Tower intact. It must be a user-visible switch with a documented fallback.",
|
|
8
|
+
"optional-config": "A tuning knob, not a capability. It stays in the config form and never hides a component.",
|
|
9
|
+
"forbidden-to-disable": "The host, the health contract, the recovery escape, the review contract and the merge gates. No group, row or boolean key may turn it off."
|
|
10
|
+
},
|
|
11
|
+
"components": [
|
|
12
|
+
{
|
|
13
|
+
"id": "tower",
|
|
14
|
+
"scope": "visible",
|
|
15
|
+
"kind": "group",
|
|
16
|
+
"fallback": "Off equals uninstalling Tower: no tools, presets or skills remain. That is the accepted all-off boundary of the mandatory host anchor, not an optional capability."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "tower-host",
|
|
20
|
+
"scope": "internal",
|
|
21
|
+
"kind": "row",
|
|
22
|
+
"fallback": "The Host plugin itself, nested under the mandatory tower anchor. A mandatory dependency, never its own switch."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "tower-skills",
|
|
26
|
+
"scope": "visible",
|
|
27
|
+
"kind": "row",
|
|
28
|
+
"fallback": "The 3 skills leave the catalog; the worker/reviewer PROTOCOL sections still ship through systemPrompt.section, so only the tower-recovery guidance is lost."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "tower-batch",
|
|
32
|
+
"scope": "visible",
|
|
33
|
+
"kind": "row",
|
|
34
|
+
"fallback": "tower_batch is not registered; missions, gates and merges are unaffected because no state depends on a batch."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "tower-import",
|
|
38
|
+
"scope": "visible",
|
|
39
|
+
"kind": "row",
|
|
40
|
+
"fallback": "tower_import_legacy and tower_legacy_map are not registered; new repository state is unaffected."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "tower-leases",
|
|
44
|
+
"scope": "visible",
|
|
45
|
+
"kind": "row",
|
|
46
|
+
"fallback": "tower_resource is not registered; advisory leases are coordination only, so nothing else depends on them."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "tower-tests",
|
|
50
|
+
"scope": "visible",
|
|
51
|
+
"kind": "row",
|
|
52
|
+
"fallback": "tower_test is not registered and no test receipt can be recorded. With gates.requireTests:true planning/mode entry is rejected with an explanation, and the merge gate still reports tests-missing."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "tower-hmr",
|
|
56
|
+
"scope": "visible",
|
|
57
|
+
"kind": "row",
|
|
58
|
+
"fallback": "tower_reload and /tower reload are unavailable; a profile without HMR behaves exactly as before. Shipped disabled: enable the row for a linked dev profile."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "preset-tower-worker",
|
|
62
|
+
"scope": "internal",
|
|
63
|
+
"kind": "row",
|
|
64
|
+
"fallback": "tower_spawn fails without the worker preset. A mandatory dependency, never a switch."
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"id": "preset-tower-reviewer",
|
|
68
|
+
"scope": "visible",
|
|
69
|
+
"kind": "row",
|
|
70
|
+
"fallback": "The reviewer cannot be dispatched. Supported while review.requireReview is false (merge is unaffected); with the gate on planning/mode entry is locked with an explanatory error."
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"gateableTools": {},
|
|
74
|
+
"keys": [
|
|
75
|
+
{
|
|
76
|
+
"name": "section",
|
|
77
|
+
"class": "core",
|
|
78
|
+
"note": "Legacy alias for mode.promptSection: the Tower-mode prompt contract, not a capability."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "allowAgentControl",
|
|
82
|
+
"class": "core",
|
|
83
|
+
"note": "Legacy alias for mode.allowAgentControl: the human-owned mode rule (ADR 0003), not a component."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "init.autoGitInit",
|
|
87
|
+
"class": "optional-config",
|
|
88
|
+
"note": "Bootstrap convenience; off only means tower_init will not run git init."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "init.snapshotDirty",
|
|
92
|
+
"class": "optional-config",
|
|
93
|
+
"note": "Dirty-work baseline convenience; no capability is removed."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"name": "init.autoExclude",
|
|
97
|
+
"class": "optional-config",
|
|
98
|
+
"note": "Keeps Tower artifacts out of .git/info/exclude; safe either way."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "mode.promptSection",
|
|
102
|
+
"class": "core",
|
|
103
|
+
"note": "Replaces the whole Tower-mode prompt text; contract text, not a capability."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "mode.sectionAppend",
|
|
107
|
+
"class": "optional-config",
|
|
108
|
+
"note": "Appends team conventions to the prompt."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "mode.allowAgentControl",
|
|
112
|
+
"class": "core",
|
|
113
|
+
"note": "Decides whether the model may toggle the mode (ADR 0003). A governance rule, never a component."
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "mode.autoPauseGoal",
|
|
117
|
+
"class": "core",
|
|
118
|
+
"note": "Entering the mode pauses an active goal; exit never resumes it."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "mode.requireResumeAuthorization",
|
|
122
|
+
"class": "core",
|
|
123
|
+
"note": "Guards resuming a user-paused mission."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "mode.requireHumanAckOnFirstEntry",
|
|
127
|
+
"class": "core",
|
|
128
|
+
"note": "First entry can require an explicit human ack."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "mode.workspaceLock",
|
|
132
|
+
"class": "core",
|
|
133
|
+
"note": "One Tower session per repository is a safety contract; a row may not turn it off."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "mode.workspaceLockScope",
|
|
137
|
+
"class": "optional-config",
|
|
138
|
+
"note": "Chooses whether read-only tools take the workspace lock."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "mode.planModePolicy",
|
|
142
|
+
"class": "core",
|
|
143
|
+
"note": "Plan mode and Tower mode stay mutually exclusive."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "mode.workspaceStaleMs",
|
|
147
|
+
"class": "optional-config",
|
|
148
|
+
"note": "Stale window for the advisory workspace lock."
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "mode.heartbeatMs",
|
|
152
|
+
"class": "optional-config",
|
|
153
|
+
"note": "Heartbeat interval while the mode is on."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "mode.denyTools",
|
|
157
|
+
"class": "core",
|
|
158
|
+
"note": "The denied-tool list is part of the mode guard."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "mode.storePath",
|
|
162
|
+
"class": "optional-config",
|
|
163
|
+
"note": "Custom mode-store location; no capability is removed."
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "mode.maxEntries",
|
|
167
|
+
"class": "optional-config",
|
|
168
|
+
"note": "Bounds the persisted mode store."
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "mode.lockTimeoutMs",
|
|
172
|
+
"class": "optional-config",
|
|
173
|
+
"note": "Bounded wait for the cross-process mode lock."
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "review.requireReview",
|
|
177
|
+
"class": "forbidden-to-disable",
|
|
178
|
+
"note": "Turns independent review into a merge blocker; no component may switch it off. With it on, the preset-tower-reviewer row may not be off (planning/mode entry refuse the combination)."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "review.maxRoundsPerReviewer",
|
|
182
|
+
"class": "forbidden-to-disable",
|
|
183
|
+
"note": "Caps review rounds; part of the review contract."
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "merge.timeoutMs",
|
|
187
|
+
"class": "optional-config",
|
|
188
|
+
"note": "Default merge timeout; a per-call value wins."
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "merge.maxBufferBytes",
|
|
192
|
+
"class": "optional-config",
|
|
193
|
+
"note": "Captured merge output buffer."
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "git.timeoutMs",
|
|
197
|
+
"class": "optional-config",
|
|
198
|
+
"note": "Default Git command timeout outside merge."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "worktrees.root",
|
|
202
|
+
"class": "optional-config",
|
|
203
|
+
"note": "Default worktree placement."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "spawn.workerPreset",
|
|
207
|
+
"class": "core",
|
|
208
|
+
"note": "Chooses the worker tool surface; a dependency, never a switch."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "spawn.reviewerPreset",
|
|
212
|
+
"class": "core",
|
|
213
|
+
"note": "Chooses the read-only reviewer surface; a dependency, never a switch."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "spawn.maxDepth",
|
|
217
|
+
"class": "optional-config",
|
|
218
|
+
"note": "Subagent depth requested at spawn."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"name": "spawn.provider",
|
|
222
|
+
"class": "optional-config",
|
|
223
|
+
"note": "Provider route for spawned agents."
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "spawn.workerModel",
|
|
227
|
+
"class": "optional-config",
|
|
228
|
+
"note": "Optional model route for workers."
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "spawn.reviewerModel",
|
|
232
|
+
"class": "optional-config",
|
|
233
|
+
"note": "Optional independent model route for reviewers."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"name": "spawn.maxLiveWorkers",
|
|
237
|
+
"class": "core",
|
|
238
|
+
"note": "Resource-safety cap on live workers."
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"name": "spawn.staleMs",
|
|
242
|
+
"class": "optional-config",
|
|
243
|
+
"note": "Roster staleness window before merge is blocked."
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"name": "spawn.maxWaitMs",
|
|
247
|
+
"class": "optional-config",
|
|
248
|
+
"note": "Upper bound for a blocking spawn wait."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "spawn.requireLease",
|
|
252
|
+
"class": "optional-config",
|
|
253
|
+
"note": "Advisory-lease gate before a worker starts."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "send.maxPerMinute",
|
|
257
|
+
"class": "optional-config",
|
|
258
|
+
"note": "Message rate limit; zero disables it."
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "send.interruptDefault",
|
|
262
|
+
"class": "optional-config",
|
|
263
|
+
"note": "Default for the interrupt-before-deliver flag."
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "tests.enabled",
|
|
267
|
+
"class": "optional-component",
|
|
268
|
+
"note": "Tuning inside the optional tower-tests component: it decides whether discovery runs when the row is on. Off with gates.requireTests:true is rejected at config resolution."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"name": "tests.admission",
|
|
272
|
+
"class": "optional-config",
|
|
273
|
+
"note": "Chooses when discovery runs inside the optional tests capability."
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"name": "tests.timeoutMs",
|
|
277
|
+
"class": "optional-config",
|
|
278
|
+
"note": "Per-command discovery timeout."
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "tests.maxBytes",
|
|
282
|
+
"class": "optional-config",
|
|
283
|
+
"note": "Captured discovery output cap."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "tests.autoChecks",
|
|
287
|
+
"class": "optional-config",
|
|
288
|
+
"note": "Attaches a tests check to completion when discovery runs."
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "tests.allowPackageDiscovery",
|
|
292
|
+
"class": "core",
|
|
293
|
+
"note": "Allows repository code to execute; a security contract, not a component."
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "gates.requireChecks",
|
|
297
|
+
"class": "forbidden-to-disable",
|
|
298
|
+
"note": "Requires completion evidence before merge; a merge gate."
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "gates.requireTests",
|
|
302
|
+
"class": "forbidden-to-disable",
|
|
303
|
+
"note": "Requires a test receipt before merge; a merge gate. With the tower-tests row off, planning/mode entry refuse the combination."
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "gates.allowReviewExemption",
|
|
307
|
+
"class": "forbidden-to-disable",
|
|
308
|
+
"note": "The review-exemption path is part of the review and merge contract."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "gates.maxReportBytes",
|
|
312
|
+
"class": "forbidden-to-disable",
|
|
313
|
+
"note": "The report cap is part of the completion contract; the refusal itself is never configurable away."
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "docs.board",
|
|
317
|
+
"class": "optional-config",
|
|
318
|
+
"note": "Writes the human-readable board; no capability is removed."
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"name": "docs.boardLocation",
|
|
322
|
+
"class": "optional-config",
|
|
323
|
+
"note": "Chooses the board location."
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "archive.enabled",
|
|
327
|
+
"class": "optional-config",
|
|
328
|
+
"note": "Archival is maintenance, not a tool capability; off only leaves terminal missions hot."
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"name": "archive.afterDays",
|
|
332
|
+
"class": "optional-config",
|
|
333
|
+
"note": "Archive age threshold."
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "archive.maxMissions",
|
|
337
|
+
"class": "optional-config",
|
|
338
|
+
"note": "Hot terminal-mission cap."
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "leases.ttlMs",
|
|
342
|
+
"class": "optional-config",
|
|
343
|
+
"note": "Tuning inside the optional tower-leases component."
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "dev.reloadDelayMs",
|
|
347
|
+
"class": "optional-config",
|
|
348
|
+
"note": "Delay so a tool result is delivered before a scheduled reload inside the optional tower-hmr component."
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "dev.reloadRequiredIds",
|
|
352
|
+
"class": "optional-config",
|
|
353
|
+
"note": "Loader ids passed to reconcileProfile as required by the tower-hmr component."
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
"forbidden": [
|
|
357
|
+
{
|
|
358
|
+
"id": "host",
|
|
359
|
+
"capability": "the Tower host",
|
|
360
|
+
"reason": "Without the host the plugin registers nothing; there is no off state that leaves the rest working, and no config boolean may remove it. The visible `tower` anchor is the all-off boundary, and the host section nested under it may never become its own switch.",
|
|
361
|
+
"tools": [],
|
|
362
|
+
"rowIds": ["tower-host"]
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"id": "health",
|
|
366
|
+
"capability": "tower_doctor (read-only health contract)",
|
|
367
|
+
"reason": "Diagnosis must always exist so a failure message can point at it. #122 removed exposeDoctor and the exposeOperational master switch; the tool is registered for every configuration.",
|
|
368
|
+
"tools": ["tower_doctor"],
|
|
369
|
+
"rowIds": ["tower-doctor"]
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"id": "recovery",
|
|
373
|
+
"capability": "tower_recover (stale state-lock escape)",
|
|
374
|
+
"reason": "The escape hatch must exist whenever the lock it repairs can be left behind. #122 removed exposeRecovery and the exposeOperational master switch; the tool is registered for every configuration.",
|
|
375
|
+
"tools": ["tower_recover"],
|
|
376
|
+
"rowIds": ["tower-recover"]
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"id": "review",
|
|
380
|
+
"capability": "the review contract",
|
|
381
|
+
"reason": "review.requireReview is a merge blocker, not a component; a row may not switch it off. The reviewer row is conditional, not a way to weaken the gate.",
|
|
382
|
+
"tools": ["tower_diff", "tower_preflight", "tower_review"],
|
|
383
|
+
"rowIds": ["tower-review"],
|
|
384
|
+
"gatedBy": ["review.requireReview", "review.maxRoundsPerReviewer"]
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"id": "merge-gates",
|
|
388
|
+
"capability": "the merge gates",
|
|
389
|
+
"reason": "gates.* decides delivery; a component may not bypass it.",
|
|
390
|
+
"tools": ["tower_complete", "tower_merge"],
|
|
391
|
+
"rowIds": ["tower-gates", "tower-merge-gates"],
|
|
392
|
+
"gatedBy": ["gates.requireChecks", "gates.requireTests", "gates.allowReviewExemption", "gates.maxReportBytes"]
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Generate the component-surface reference from the single source of truth
|
|
2
|
+
// (#120). The table is a rendering of `scripts/component-surface.json`; editing
|
|
3
|
+
// the markdown by hand fails `npm run docs:check:generated:components`.
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// node scripts/generate-component-surface.mjs [--root <dir>] [--check]
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import {
|
|
11
|
+
COMPONENT_REFERENCE_FILE,
|
|
12
|
+
COMPONENT_SURFACE_FILE,
|
|
13
|
+
parseComponentSurface,
|
|
14
|
+
renderComponentSurface,
|
|
15
|
+
} from '../domain/components.js'
|
|
16
|
+
import { readBaseline } from './lib/surface.mjs'
|
|
17
|
+
import { parseArgs, runMain } from './lib/cli.mjs'
|
|
18
|
+
|
|
19
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const USAGE = `usage: node scripts/generate-component-surface.mjs [--root <dir>] [--check]
|
|
21
|
+
|
|
22
|
+
--root repository root to read and write (default: this package)
|
|
23
|
+
--check fail when the committed reference differs instead of writing it
|
|
24
|
+
--help print this text
|
|
25
|
+
|
|
26
|
+
The table is generated from ${COMPONENT_SURFACE_FILE}; do not edit
|
|
27
|
+
${COMPONENT_REFERENCE_FILE} by hand.`
|
|
28
|
+
|
|
29
|
+
function main(argv) {
|
|
30
|
+
const options = parseArgs(argv, {
|
|
31
|
+
initial: { root: PACKAGE_ROOT, check: false, help: false },
|
|
32
|
+
values: { '--root': { key: 'root', parse: path.resolve } },
|
|
33
|
+
flags: { '--check': 'check', '--help': 'help', '-h': 'help' },
|
|
34
|
+
})
|
|
35
|
+
if (options.help) {
|
|
36
|
+
console.log(USAGE)
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
const root = path.resolve(options.root)
|
|
40
|
+
const surface = parseComponentSurface(fs.readFileSync(path.join(root, COMPONENT_SURFACE_FILE), 'utf8'))
|
|
41
|
+
const baseline = readBaseline(root)
|
|
42
|
+
const expected = renderComponentSurface(surface, { baselineDate: baseline.date })
|
|
43
|
+
const target = path.join(root, COMPONENT_REFERENCE_FILE)
|
|
44
|
+
if (options.check) {
|
|
45
|
+
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
46
|
+
if (actual !== expected) {
|
|
47
|
+
console.error(`component reference is stale; run npm run docs:generate:components (${COMPONENT_REFERENCE_FILE})`)
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
console.log(`component reference: ${surface.keys.length} keys in sync (${COMPONENT_REFERENCE_FILE})`)
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
54
|
+
fs.writeFileSync(target, expected)
|
|
55
|
+
console.log(`wrote ${COMPONENT_REFERENCE_FILE} (${surface.keys.length} keys, ${surface.components.length} components)`)
|
|
56
|
+
return 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'components', usage: USAGE })
|
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
// Generate the tools overview from the registered tool definitions (#46).
|
|
2
|
+
//
|
|
3
|
+
// #118: use the shipped Host entry, not only the define() path, so the table and
|
|
4
|
+
// its header count include tower_mode/tower_control as well.
|
|
2
5
|
import fs from 'node:fs'
|
|
3
6
|
import path from 'node:path'
|
|
4
7
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { registerTowerTools } from '../tools/index.js'
|
|
6
8
|
import { SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
|
|
9
|
+
import { loadShippedToolSurface } from './lib/tool-surface.mjs'
|
|
7
10
|
import { readBaseline } from './lib/surface.mjs'
|
|
8
11
|
|
|
9
12
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
10
13
|
const target = path.join(root, 'docs', 'reference', 'tools.generated.md')
|
|
11
14
|
const baseline = readBaseline(root)
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
get() {
|
|
15
|
-
return undefined
|
|
16
|
-
},
|
|
17
|
-
effect(fn) {
|
|
18
|
-
const dispose = fn()
|
|
19
|
-
return typeof dispose === 'function' ? dispose : () => {}
|
|
20
|
-
},
|
|
21
|
-
systemPrompt: { section() {} },
|
|
22
|
-
tools: {
|
|
23
|
-
register(definition) {
|
|
24
|
-
tools.set(definition.name, definition)
|
|
25
|
-
return () => {}
|
|
26
|
-
},
|
|
27
|
-
guard() {
|
|
28
|
-
return () => {}
|
|
29
|
-
},
|
|
30
|
-
restrict() {
|
|
31
|
-
return () => {}
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
registerTowerTools(ctx)
|
|
15
|
+
const surface = loadShippedToolSurface()
|
|
16
|
+
const tools = surface.definitions
|
|
36
17
|
const rows = [...tools.values()]
|
|
37
18
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
38
19
|
.map((tool) => `| \`${tool.name}\` | ${String(tool.description ?? '').replace(/\s+/g, ' ').trim()} |`)
|
|
@@ -52,6 +33,7 @@ const expected = `# Tools overview (generated)
|
|
|
52
33
|
|---|---|
|
|
53
34
|
${rows.join('\n')}
|
|
54
35
|
`
|
|
36
|
+
surface.dispose()
|
|
55
37
|
if (process.argv.includes('--check')) {
|
|
56
38
|
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
57
39
|
if (actual !== expected) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Generate the published type vocabulary from the runtime constants (#115).
|
|
2
|
+
//
|
|
3
|
+
// `types/index.d.ts` is the shipped type surface (`package.json` `types`). Its
|
|
4
|
+
// status/type/role/error unions used to be a hand-copied second declaration of
|
|
5
|
+
// constants that live in domain/; this generator makes the runtime the single
|
|
6
|
+
// source: the marked block is rewritten from ERROR_CODES, MISSION_STATUSES,
|
|
7
|
+
// REVIEW_STATUSES, MERGE_VERDICTS, MISSION_KINDS, REVIEW_POLICIES, FINDING_*,
|
|
8
|
+
// ROLES and PAUSE_CAUSES. It is the type-surface sibling of the four
|
|
9
|
+
// `docs:generate:*` generators and shares their `--check` contract.
|
|
10
|
+
//
|
|
11
|
+
// Usage: node scripts/generate-types.mjs [--check]
|
|
12
|
+
//
|
|
13
|
+
// The block is delimited by `// generated:vocabulary:start` / `:end` in
|
|
14
|
+
// types/index.d.ts; the prose around it is hand-written. A missing marker is a
|
|
15
|
+
// configuration error, not a silent no-op.
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
import { ERROR_CODES } from '../domain/errors.js'
|
|
20
|
+
import {
|
|
21
|
+
FINDING_SEVERITIES,
|
|
22
|
+
FINDING_STATUSES,
|
|
23
|
+
FINDING_TYPES,
|
|
24
|
+
MERGE_VERDICTS,
|
|
25
|
+
MISSION_STATUSES,
|
|
26
|
+
PAUSE_CAUSES,
|
|
27
|
+
REVIEW_STATUSES,
|
|
28
|
+
} from '../domain/mission.js'
|
|
29
|
+
import { MISSION_KINDS, REVIEW_POLICIES, ROLES } from '../domain/vocabulary.js'
|
|
30
|
+
|
|
31
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
+
const TARGET = path.join(root, 'types', 'index.d.ts')
|
|
33
|
+
export const START = '// generated:vocabulary:start'
|
|
34
|
+
export const END = '// generated:vocabulary:end'
|
|
35
|
+
|
|
36
|
+
/** One published alias per runtime constant family, in declaration order. */
|
|
37
|
+
const DECLARATIONS = [
|
|
38
|
+
{ key: 'missionStatuses', name: 'MissionStatus' },
|
|
39
|
+
{ key: 'missionKinds', name: 'MissionKind' },
|
|
40
|
+
{ key: 'reviewStatuses', name: 'ReviewStatus', multiline: true },
|
|
41
|
+
{ key: 'mergeVerdicts', name: 'MergeVerdict' },
|
|
42
|
+
{ key: 'reviewPolicies', name: 'ReviewPolicy' },
|
|
43
|
+
{ key: 'findingTypes', name: 'FindingType' },
|
|
44
|
+
{ key: 'findingSeverities', name: 'FindingSeverity' },
|
|
45
|
+
{ key: 'findingStatuses', name: 'FindingStatus' },
|
|
46
|
+
{ key: 'roles', name: 'Role' },
|
|
47
|
+
{ key: 'pauseCauses', name: 'PauseCause' },
|
|
48
|
+
{ key: 'errorCodes', name: 'ErrorCode', multiline: true },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/** The runtime data the block is generated from. `--check` compares this exact input. */
|
|
52
|
+
export function vocabularyData() {
|
|
53
|
+
return {
|
|
54
|
+
missionStatuses: [...MISSION_STATUSES],
|
|
55
|
+
missionKinds: [...MISSION_KINDS],
|
|
56
|
+
reviewStatuses: [...REVIEW_STATUSES],
|
|
57
|
+
mergeVerdicts: [...MERGE_VERDICTS],
|
|
58
|
+
reviewPolicies: [...REVIEW_POLICIES],
|
|
59
|
+
findingTypes: [...FINDING_TYPES],
|
|
60
|
+
findingSeverities: [...FINDING_SEVERITIES],
|
|
61
|
+
findingStatuses: [...FINDING_STATUSES],
|
|
62
|
+
roles: [...ROLES],
|
|
63
|
+
pauseCauses: [...PAUSE_CAUSES],
|
|
64
|
+
errorCodes: Object.keys(ERROR_CODES),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderDeclaration({ name, values, multiline = false }) {
|
|
69
|
+
const quoted = values.map((value) => `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`)
|
|
70
|
+
const inline = `export type ${name} = ${quoted.join(' | ')}`
|
|
71
|
+
if (!multiline && inline.length <= 120) return inline
|
|
72
|
+
return [`export type ${name} =`, ...quoted.map((value) => ` | ${value}`)].join('\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Render the marked block (markers excluded) for one vocabulary input.
|
|
77
|
+
* @param {Record<string, string[]>} data
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function vocabularyBlock(data) {
|
|
81
|
+
return DECLARATIONS.map((declaration) =>
|
|
82
|
+
renderDeclaration({ ...declaration, values: data[declaration.key] }),
|
|
83
|
+
).join('\n')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Replace the marked block in a declaration file. Removing or reordering the
|
|
88
|
+
* markers is a hard error: a generated block that cannot be found would
|
|
89
|
+
* otherwise pass every check while drifting.
|
|
90
|
+
* @param {string} source
|
|
91
|
+
* @param {string} block
|
|
92
|
+
* @returns {string}
|
|
93
|
+
*/
|
|
94
|
+
export function injectVocabulary(source, block) {
|
|
95
|
+
const start = source.indexOf(START)
|
|
96
|
+
const end = source.indexOf(END)
|
|
97
|
+
if (start === -1 || end === -1 || end < start) {
|
|
98
|
+
throw new Error(`types/index.d.ts is missing the ${START} / ${END} markers`)
|
|
99
|
+
}
|
|
100
|
+
return `${source.slice(0, start + START.length)}\n${block}\n${source.slice(end)}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function main() {
|
|
104
|
+
const source = fs.readFileSync(TARGET, 'utf8')
|
|
105
|
+
const expected = injectVocabulary(source, vocabularyBlock(vocabularyData()))
|
|
106
|
+
const codes = Object.keys(ERROR_CODES).length
|
|
107
|
+
if (process.argv.includes('--check')) {
|
|
108
|
+
if (source !== expected) {
|
|
109
|
+
console.error('published type vocabulary is stale; run npm run types:generate')
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
console.log(`type vocabulary: ${DECLARATIONS.length} aliases (${codes} error codes) in sync`)
|
|
113
|
+
} else {
|
|
114
|
+
fs.writeFileSync(TARGET, expected)
|
|
115
|
+
console.log(`wrote ${path.relative(root, TARGET)} (${DECLARATIONS.length} aliases, ${codes} error codes)`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
120
|
+
if (isMain) {
|
|
121
|
+
try {
|
|
122
|
+
main()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`type vocabulary: ${String(error?.message ?? error)}`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
}
|