dsh-tower 2.1.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 +137 -0
- package/COMPATIBILITY.md +6 -1
- package/CONTRIBUTING.md +78 -5
- package/PRINCIPLES.md +163 -0
- package/README.en.md +35 -19
- package/README.md +28 -18
- package/SECURITY.md +6 -0
- package/{lib → adapters}/archive.js +1 -1
- package/{lib → adapters}/caseSensitivity.js +1 -1
- package/adapters/default.js +51 -10
- package/{lib → adapters}/git.js +1 -1
- package/{lib → adapters}/leases.js +1 -1
- package/{lib → adapters}/legacy.js +1 -1
- package/adapters/persistence.js +543 -0
- package/adapters/store-fs.js +28 -1
- package/adapters/store.js +204 -0
- package/{lib → adapters}/tests.js +9 -5
- package/adapters/vcs-git.js +15 -1
- package/{lib → adapters}/workspaceLock.js +1 -1
- package/{lib → adapters}/worktreeOwnership.js +1 -1
- package/app/archive.js +4 -6
- package/app/batch.js +9 -10
- package/app/board.js +2 -4
- package/app/checkCompletion.js +9 -10
- package/app/enrichedMissions.js +2 -4
- package/app/findings.js +6 -7
- package/app/gate.js +1 -2
- package/app/init.js +24 -29
- package/app/merge.js +58 -51
- package/app/messages.js +7 -8
- package/app/mission.js +5 -5
- package/app/plan.js +8 -5
- package/app/provision.js +12 -14
- package/app/read.js +6 -3
- package/app/rebase.js +14 -16
- package/app/recording.js +7 -8
- package/app/review.js +15 -8
- package/app/roster.js +14 -15
- package/{lib → app}/spawn-prechecks.js +4 -3
- package/app/spawn.js +12 -13
- package/app/teardown.js +8 -9
- package/app/tower.js +89 -35
- package/cordis.patch.yml +56 -28
- package/docs/README.md +7 -2
- package/docs/adr/0001-ports-adapters-as-data.md +49 -0
- package/docs/adr/0002-single-state-json.md +50 -0
- package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
- package/docs/adr/0004-surface-budget.md +70 -0
- package/docs/adr/0005-component-surface.md +130 -0
- package/docs/adr/README.md +52 -0
- package/docs/agents/workflow.md +51 -0
- package/docs/architecture/overview.md +192 -43
- package/docs/guide/hot-reload.md +5 -5
- package/docs/guide/install-upgrade-migration.md +13 -0
- package/docs/guide/release.md +1 -1
- package/docs/operations/incident.md +82 -0
- package/docs/operations/runbook.md +12 -3
- package/docs/operations/security.md +1 -1
- package/docs/reference/blockers.md +2 -2
- package/docs/reference/component-surface.md +135 -0
- package/docs/reference/config.generated.md +2 -9
- package/docs/reference/config.md +11 -9
- package/docs/reference/doctor.md +53 -2
- package/docs/reference/errors.generated.md +41 -37
- package/docs/reference/errors.md +53 -41
- package/docs/reference/events.md +38 -6
- package/docs/reference/surface-budget.md +157 -0
- package/docs/reference/tools.generated.md +8 -5
- package/docs/reference/tools.md +10 -7
- package/docs/zh/README.md +1 -1
- package/docs/zh/guide/hot-reload.md +4 -4
- package/docs/zh/guide/install-upgrade-migration.md +10 -0
- package/docs/zh/operations/security.md +1 -1
- package/docs/zh/reference/blockers.md +2 -2
- package/docs/zh/reference/config.md +9 -9
- package/docs/zh/reference/doctor.md +29 -1
- package/docs/zh/reference/tools.md +10 -8
- package/domain/batch.js +2 -2
- package/domain/components.js +641 -0
- package/domain/config/schema.js +62 -28
- package/domain/contracts/outputs.js +16 -2
- package/domain/contracts/tool-inputs/ops.js +1 -0
- package/domain/errors.js +308 -43
- package/domain/events.js +207 -0
- package/domain/integration/gate.js +17 -2
- package/domain/integration/merge.js +35 -0
- package/domain/lockBudget.js +30 -0
- package/domain/mission.js +12 -1
- package/domain/missions/completion.js +14 -0
- package/domain/missions/lifecycle.js +9 -3
- package/domain/missions/plan.js +14 -0
- package/domain/missions/review.js +90 -7
- package/domain/read/views.js +3 -0
- package/domain/state/archive.js +1 -1
- package/domain/state.js +1 -1
- package/domain/surface.js +347 -0
- 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/{lib → host}/doctor.js +157 -18
- package/{lib → host}/hmr.js +2 -4
- package/{lib → host}/modeStore.js +1 -1
- package/host/presets.js +77 -0
- package/{lib → host}/tool-registry.js +235 -44
- package/host/tower.js +32 -0
- package/index.js +40 -25
- package/locale/en.json +31 -11
- package/locale/zh.json +30 -10
- package/package.json +24 -4
- package/ports/archive.js +18 -0
- package/ports/caseSensitivity.js +15 -0
- package/ports/clock.js +10 -2
- package/ports/config.js +19 -0
- package/ports/contract.js +38 -0
- package/ports/events.js +24 -0
- package/ports/ids.js +10 -2
- package/ports/index.js +54 -2
- package/ports/ownership.js +17 -0
- package/ports/state.js +16 -0
- package/ports/store.js +52 -3
- package/ports/vcs.js +25 -4
- package/scripts/architecture-waivers.json +19 -0
- package/scripts/architecture.mjs +634 -23
- package/scripts/check-components.mjs +235 -0
- package/scripts/check-docs.mjs +8 -35
- package/scripts/check-pack.mjs +68 -39
- package/scripts/check-philosophy.mjs +266 -0
- package/scripts/component-surface.json +395 -0
- package/scripts/domain-metrics.mjs +13 -4
- package/scripts/generate-component-surface.mjs +59 -0
- package/scripts/generate-config-reference.mjs +14 -1
- package/scripts/generate-error-reference.mjs +68 -14
- package/scripts/generate-events-reference.mjs +83 -0
- package/scripts/generate-tools-reference.mjs +18 -25
- package/scripts/generate-types.mjs +127 -0
- package/scripts/issue-audit.mjs +2 -2
- package/scripts/lib/surface.mjs +87 -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 +75 -22
- package/scripts/regression-audit.mjs +164 -0
- package/scripts/release-check.mjs +1 -1
- package/scripts/surface-baseline.json +52 -0
- package/scripts/surface-baseline.mjs +118 -0
- 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/adopt.js +2 -3
- package/tools/batch.js +2 -3
- package/tools/complete.js +2 -3
- package/tools/diff.js +3 -3
- package/tools/doctor.js +7 -10
- package/tools/finding.js +2 -3
- package/tools/findings.js +3 -3
- package/tools/import-legacy.js +3 -5
- package/tools/inbox.js +3 -3
- package/tools/index.js +9 -8
- package/tools/init.js +3 -6
- package/tools/interrupt.js +2 -3
- package/tools/legacy-map.js +3 -5
- package/tools/merge.js +3 -3
- package/tools/mission.js +2 -3
- package/tools/plan.js +6 -5
- package/tools/preflight.js +2 -3
- package/tools/rebase.js +3 -3
- package/tools/recover.js +38 -13
- package/tools/reload.js +36 -40
- package/tools/resource.js +3 -5
- package/tools/review.js +3 -4
- package/tools/roster.js +2 -3
- package/tools/send.js +2 -2
- package/tools/spawn.js +8 -5
- package/tools/status.js +11 -4
- package/tools/teardown.js +2 -3
- package/tools/test.js +2 -2
- package/types/index.d.ts +29 -13
- package/ui-bundle/README.md +37 -3
- package/ui-bundle/client.cards.js +356 -0
- package/ui-bundle/client.denied.js +142 -0
- package/ui-bundle/client.dock.js +333 -0
- package/ui-bundle/client.helpers.js +271 -0
- package/ui-bundle/client.js +79 -1747
- package/ui-bundle/client.locales.js +247 -0
- package/ui-bundle/client.mode.js +206 -0
- package/ui-bundle/client.overlay.js +64 -0
- package/ui-bundle/client.styles.js +151 -0
- package/ui-bundle/client.toolview.js +82 -0
- package/ui-bundle/package.json +1 -0
- package/lib/persistence.js +0 -356
- package/lib/presets.js +0 -35
- package/lib/store.js +0 -133
- /package/{lib → adapters}/stateFile.js +0 -0
- /package/{lib → app}/briefing.js +0 -0
- /package/{lib → app}/spawn-support.js +0 -0
- /package/{lib → host}/compatibility.js +0 -0
- /package/{lib → host}/config.js +0 -0
- /package/{lib → host}/modeState.js +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Component-surface policy gate (#120).
|
|
3
|
+
//
|
|
4
|
+
// Principle 11 says optional capabilities are switches and the core is not. This
|
|
5
|
+
// gate is what makes that a rule instead of a slogan. It fails on:
|
|
6
|
+
//
|
|
7
|
+
// 1. a visible component without `title`, `summary` and `disables` in both
|
|
8
|
+
// locale/en.json and locale/zh.json;
|
|
9
|
+
// 2. a forbidden-to-disable capability (host, health, recovery, review
|
|
10
|
+
// contract, merge gates) that any group, row or boolean key can disable;
|
|
11
|
+
// 3. a component count that moved without a decision in
|
|
12
|
+
// scripts/surface-baseline.json (the same brake `surface:check` applies).
|
|
13
|
+
//
|
|
14
|
+
// It also refuses the quiet forms of the same drift: a shipped component row
|
|
15
|
+
// missing from the single source of truth, a live config key without a
|
|
16
|
+
// component-axis class, and a generated reference that is out of date.
|
|
17
|
+
//
|
|
18
|
+
// The single source of truth is `scripts/component-surface.json`; the generated
|
|
19
|
+
// table is `docs/reference/component-surface.md`; the policy is recorded in
|
|
20
|
+
// `docs/adr/0005-component-surface.md`.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/check-components.mjs [--root <dir>] [--json]
|
|
24
|
+
import fs from 'node:fs'
|
|
25
|
+
import path from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { Config, configSurface } from '../domain/config/schema.js'
|
|
28
|
+
import {
|
|
29
|
+
COMPONENT_REFERENCE_FILE,
|
|
30
|
+
COMPONENT_SURFACE_FILE,
|
|
31
|
+
LOCALE_FILES,
|
|
32
|
+
checkForbidden,
|
|
33
|
+
checkLocaleCoverage,
|
|
34
|
+
parseComponentSurface,
|
|
35
|
+
parseCordisPatch,
|
|
36
|
+
parseOperationalRegistry,
|
|
37
|
+
renderComponentSurface,
|
|
38
|
+
validateComponentSurface,
|
|
39
|
+
} from '../domain/components.js'
|
|
40
|
+
import { validateSurfaceBaselineKey } from '../domain/surface.js'
|
|
41
|
+
import { readBaseline } from './lib/surface.mjs'
|
|
42
|
+
import { parseArgs, runMain } from './lib/cli.mjs'
|
|
43
|
+
|
|
44
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
45
|
+
const REGISTRY_FILE = 'host/tool-registry.js'
|
|
46
|
+
const PATCH_FILE = 'cordis.patch.yml'
|
|
47
|
+
const USAGE = `usage: node scripts/check-components.mjs [--root <dir>] [--json]
|
|
48
|
+
|
|
49
|
+
--root repository root to check (default: this package)
|
|
50
|
+
--json print the report as JSON
|
|
51
|
+
--help print this text
|
|
52
|
+
|
|
53
|
+
Fails on a visible component without title/summary/disables in both locale files,
|
|
54
|
+
a forbidden-to-disable capability disabled by a group, row or boolean key, a
|
|
55
|
+
component count that moved without a scripts/surface-baseline.json decision,
|
|
56
|
+
an unclassified config key or component row, and a stale generated reference.
|
|
57
|
+
Regenerate the table with \`npm run docs:generate:components\`.`
|
|
58
|
+
|
|
59
|
+
function finding(code, level, message) {
|
|
60
|
+
return { code, level, message }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The live boolean config keys, derived from the schemastery schema. */
|
|
64
|
+
function liveBooleanKeys() {
|
|
65
|
+
const keys = []
|
|
66
|
+
for (const [key, schema] of Object.entries(Config.dict ?? {})) {
|
|
67
|
+
const dict = schema?.dict
|
|
68
|
+
if (dict && typeof dict === 'object') {
|
|
69
|
+
for (const [child, childSchema] of Object.entries(dict)) {
|
|
70
|
+
if (childSchema?.type === 'boolean') keys.push(`${key}.${child}`)
|
|
71
|
+
}
|
|
72
|
+
} else if (schema?.type === 'boolean') {
|
|
73
|
+
keys.push(key)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return keys
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The static wiring that lets a boolean key remove a tool: the OPERATIONAL_TOOLS
|
|
81
|
+
* table in host/tool-registry.js. Parsing it (rather than importing the runtime)
|
|
82
|
+
* keeps the denylist check offline and free of a booted Host. The parser accepts
|
|
83
|
+
* both the tuple and the `Map.set(...)` forms, and a trailing comma (F11), so a
|
|
84
|
+
* reformatted table cannot fall out of the allowlist.
|
|
85
|
+
*/
|
|
86
|
+
function operationalRegistry(root) {
|
|
87
|
+
const file = path.join(root, REGISTRY_FILE)
|
|
88
|
+
const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
89
|
+
return parseOperationalRegistry(text)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Read the live inputs the pure checks need: schema, patch, locales, registry. */
|
|
93
|
+
export function componentCheckData(root) {
|
|
94
|
+
const config = configSurface()
|
|
95
|
+
const patchFile = path.join(root, PATCH_FILE)
|
|
96
|
+
const patch = fs.existsSync(patchFile)
|
|
97
|
+
? parseCordisPatch(fs.readFileSync(patchFile, 'utf8'))
|
|
98
|
+
: { rows: [], visible: [] }
|
|
99
|
+
const locales = {}
|
|
100
|
+
for (const file of LOCALE_FILES) {
|
|
101
|
+
const localeFile = path.join(root, file)
|
|
102
|
+
locales[file] = fs.existsSync(localeFile) ? JSON.parse(fs.readFileSync(localeFile, 'utf8')) : null
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
configKeys: config.keys,
|
|
106
|
+
sections: config.sections,
|
|
107
|
+
booleans: liveBooleanKeys(),
|
|
108
|
+
registry: operationalRegistry(root),
|
|
109
|
+
patch,
|
|
110
|
+
locales,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run every component-surface rule against one repository root.
|
|
116
|
+
* @param {string} root
|
|
117
|
+
* @returns {{ ok: boolean, surface: any, live: any, findings: Array<{ code: string, level: string, message: string }> }}
|
|
118
|
+
*/
|
|
119
|
+
export function checkComponents(root) {
|
|
120
|
+
const findings = []
|
|
121
|
+
const live = componentCheckData(root)
|
|
122
|
+
const surfaceFile = path.join(root, COMPONENT_SURFACE_FILE)
|
|
123
|
+
if (!fs.existsSync(surfaceFile)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
surface: null,
|
|
127
|
+
live,
|
|
128
|
+
findings: [finding('surface-missing', 'error', `${COMPONENT_SURFACE_FILE} is missing`)],
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
let surface
|
|
132
|
+
try {
|
|
133
|
+
surface = parseComponentSurface(fs.readFileSync(surfaceFile, 'utf8'))
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
surface: null,
|
|
138
|
+
live,
|
|
139
|
+
findings: [finding('surface-invalid', 'error', `${COMPONENT_SURFACE_FILE}: ${String(error?.message ?? error)}`)],
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
findings.push(...validateComponentSurface(surface, { configKeys: live.configKeys, patch: live.patch }))
|
|
143
|
+
findings.push(...checkLocaleCoverage(surface, live.locales))
|
|
144
|
+
findings.push(...checkForbidden(surface, { booleans: live.booleans, registry: live.registry, patch: live.patch }))
|
|
145
|
+
|
|
146
|
+
// #122: an empty OPERATIONAL_TOOLS table is the expected state. Every route
|
|
147
|
+
// that does exist must still match the `gateableTools` allowlist, which
|
|
148
|
+
// checkForbidden enforces above; there is no "unreadable registry" failure to
|
|
149
|
+
// raise when the correct count is zero.
|
|
150
|
+
|
|
151
|
+
// The decision record must exist: the ADR is where the policy is argued.
|
|
152
|
+
if (!fs.existsSync(path.join(root, surface.adr))) {
|
|
153
|
+
findings.push(finding('adr-missing', 'error', `component surface names ${surface.adr}, which does not exist`))
|
|
154
|
+
}
|
|
155
|
+
for (const capability of surface.forbidden) {
|
|
156
|
+
const decision = capability.acceptedSwitches?.decision
|
|
157
|
+
if (!decision) continue
|
|
158
|
+
if (!fs.existsSync(path.join(root, decision.ref))) {
|
|
159
|
+
findings.push(
|
|
160
|
+
finding(
|
|
161
|
+
'forbidden-decision',
|
|
162
|
+
'error',
|
|
163
|
+
`forbidden capability ${capability.id} records a decision in ${decision.ref}, which does not exist`,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check 3: the component count is a baselined surface. `surface:check` runs
|
|
170
|
+
// the full baseline; this narrow call keeps the component brake available in
|
|
171
|
+
// this gate too, so a moved count fails here without re-measuring the rest.
|
|
172
|
+
const visible = live.patch.visible
|
|
173
|
+
let baseline = null
|
|
174
|
+
try {
|
|
175
|
+
baseline = readBaseline(root)
|
|
176
|
+
findings.push(
|
|
177
|
+
...validateSurfaceBaselineKey(
|
|
178
|
+
baseline,
|
|
179
|
+
'components',
|
|
180
|
+
{ components: visible.length, liveNames: { components: visible } },
|
|
181
|
+
{ exists: (relative) => fs.existsSync(path.join(root, relative)) },
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
} catch (error) {
|
|
185
|
+
findings.push(finding('baseline-unreadable', 'error', `scripts/surface-baseline.json is unreadable: ${String(error?.message ?? error)}`))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The reference is a rendering, not prose. There is exactly one way to update
|
|
189
|
+
// it: edit the source of truth and run the generator.
|
|
190
|
+
try {
|
|
191
|
+
const expected = renderComponentSurface(surface, { baselineDate: baseline?.date })
|
|
192
|
+
const referenceFile = path.join(root, COMPONENT_REFERENCE_FILE)
|
|
193
|
+
const actual = fs.existsSync(referenceFile) ? fs.readFileSync(referenceFile, 'utf8') : ''
|
|
194
|
+
if (actual !== expected) {
|
|
195
|
+
findings.push(
|
|
196
|
+
finding('reference-stale', 'error', `${COMPONENT_REFERENCE_FILE} is stale; run npm run docs:generate:components`),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
findings.push(finding('reference-unreadable', 'error', `${COMPONENT_REFERENCE_FILE} cannot be rendered: ${String(error?.message ?? error)}`))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { ok: findings.every((entry) => entry.level !== 'error'), surface, live, findings }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function main(argv) {
|
|
207
|
+
const options = parseArgs(argv, {
|
|
208
|
+
initial: { root: PACKAGE_ROOT, json: false, help: false },
|
|
209
|
+
values: { '--root': { key: 'root', parse: path.resolve } },
|
|
210
|
+
flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
|
|
211
|
+
})
|
|
212
|
+
if (options.help) {
|
|
213
|
+
console.log(USAGE)
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
const root = path.resolve(options.root)
|
|
217
|
+
const report = checkComponents(root)
|
|
218
|
+
if (options.json) {
|
|
219
|
+
console.log(JSON.stringify({ root, ...report }, null, 2))
|
|
220
|
+
return report.ok ? 0 : 1
|
|
221
|
+
}
|
|
222
|
+
if (report.surface) {
|
|
223
|
+
const visible = report.surface.components.filter((component) => component.scope === 'visible').length
|
|
224
|
+
const internal = report.surface.components.length - visible
|
|
225
|
+
console.log(
|
|
226
|
+
`components: ${visible} visible, ${internal} internal, ${report.surface.keys.length} config keys classified (${COMPONENT_SURFACE_FILE})`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
for (const item of report.findings) console.log(`components: ${item.level} [${item.code}] ${item.message}`)
|
|
230
|
+
if (report.ok) console.log('components: ok — locale coverage, the forbidden denylist and the component baseline hold')
|
|
231
|
+
else console.log(`components: ${report.findings.length} problem(s); see docs/adr/0005-component-surface.md`)
|
|
232
|
+
return report.ok ? 0 : 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'components', usage: USAGE })
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { EVENT_COUNT, LEGACY_TOOL_EVENT_NAMES } from '../domain/events.js'
|
|
7
8
|
|
|
8
9
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
10
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
|
@@ -15,7 +16,7 @@ const toolModuleSources = fs.existsSync(path.join(root, 'tools'))
|
|
|
15
16
|
.map((name) => read(`tools/${name}`))
|
|
16
17
|
.join('\n')
|
|
17
18
|
: ''
|
|
18
|
-
const toolsSource = read('tools/index.js') + toolModuleSources + read('
|
|
19
|
+
const toolsSource = read('tools/index.js') + toolModuleSources + read('host/tool-registry.js') + read('index.js')
|
|
19
20
|
const documentedTools = read('docs/reference/tools.md')
|
|
20
21
|
const toolNames = [...new Set([...toolsSource.matchAll(/name: '(tower_[a-z_]+)'/g)].map((match) => match[1]))]
|
|
21
22
|
.filter((name) => name !== 'tower-control')
|
|
@@ -62,37 +63,9 @@ for (const code of [
|
|
|
62
63
|
]) blockerCodes.add(code)
|
|
63
64
|
const missingBlockers = [...blockerCodes].filter((code) => !blockersSource.includes(`\`${code}\``)).sort()
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'mission',
|
|
69
|
-
'spawn',
|
|
70
|
-
'complete',
|
|
71
|
-
'review',
|
|
72
|
-
'merge',
|
|
73
|
-
'rebase',
|
|
74
|
-
'teardown',
|
|
75
|
-
'finding',
|
|
76
|
-
'findings',
|
|
77
|
-
'send',
|
|
78
|
-
'inbox',
|
|
79
|
-
'interrupt',
|
|
80
|
-
'diff',
|
|
81
|
-
'adopt',
|
|
82
|
-
'roster',
|
|
83
|
-
'status',
|
|
84
|
-
'preflight',
|
|
85
|
-
'doctor',
|
|
86
|
-
'recover',
|
|
87
|
-
'test',
|
|
88
|
-
'batch',
|
|
89
|
-
'resource',
|
|
90
|
-
'import_legacy',
|
|
91
|
-
'legacy_map',
|
|
92
|
-
'mode',
|
|
93
|
-
'control',
|
|
94
|
-
'reload',
|
|
95
|
-
]
|
|
66
|
+
// #75: the legacy alias list comes from the same event table the generated
|
|
67
|
+
// reference and the runtime use, so a renamed alias fails here first.
|
|
68
|
+
const eventNames = LEGACY_TOOL_EVENT_NAMES.map((name) => name.replace(/^tower\//, ''))
|
|
96
69
|
const documentedEvents = read('docs/reference/events.md')
|
|
97
70
|
const missingEvents = eventNames.filter((name) => !documentedEvents.includes(`\`tower/${name}\``))
|
|
98
71
|
|
|
@@ -128,8 +101,8 @@ if (!blockersDoc.includes(`${blockerCodes.size} stable blocker/failure codes`))
|
|
|
128
101
|
if (!errorsDoc.includes(`${errorCodes.length} tool error codes`)) {
|
|
129
102
|
problems.push(`docs/reference/errors.md must state ${errorCodes.length} tool error codes`)
|
|
130
103
|
}
|
|
131
|
-
if (!eventsDoc.includes(`${
|
|
132
|
-
problems.push(`docs/reference/events.md must state ${
|
|
104
|
+
if (!eventsDoc.includes(`${EVENT_COUNT} process-local events`)) {
|
|
105
|
+
problems.push(`docs/reference/events.md must state ${EVENT_COUNT} process-local events`)
|
|
133
106
|
}
|
|
134
107
|
for (const required of [
|
|
135
108
|
'| `tests.admission` | `off` |',
|
|
@@ -172,5 +145,5 @@ if (problems.length > 0) {
|
|
|
172
145
|
process.exit(1)
|
|
173
146
|
}
|
|
174
147
|
console.log(
|
|
175
|
-
`docs ok: ${toolNames.length} tools, ${blockerCodes.size} blocker codes, ${errorCodes.length} error codes, ${
|
|
148
|
+
`docs ok: ${toolNames.length} tools, ${blockerCodes.size} blocker codes, ${errorCodes.length} error codes, ${EVENT_COUNT} events, version ${packageJson.version}`,
|
|
176
149
|
)
|
package/scripts/check-pack.mjs
CHANGED
|
@@ -1,49 +1,78 @@
|
|
|
1
1
|
// Fail when the published file list drifts from the reviewed snapshot.
|
|
2
|
-
// Run: node scripts/check-pack.mjs [--write]
|
|
2
|
+
// Run: node scripts/check-pack.mjs [--write] [--root <dir>]
|
|
3
|
+
//
|
|
4
|
+
// #71: the optional Web client ships inside the main package, so the required
|
|
5
|
+
// list asserts the client package manifest and its bundle patch — not just the
|
|
6
|
+
// directory's index.js. `requiredMissing` is exported so a fixture can prove
|
|
7
|
+
// that removing one of them fails the check; `--root` lets that fixture run the
|
|
8
|
+
// real gate against a temporary package.
|
|
3
9
|
import { execFileSync } from 'node:child_process'
|
|
4
10
|
import fs from 'node:fs'
|
|
5
11
|
import path from 'node:path'
|
|
6
12
|
import { fileURLToPath } from 'node:url'
|
|
7
13
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
|
+
const REQUIRED_FILES = [
|
|
16
|
+
'cordis.patch.yml',
|
|
17
|
+
'index.js',
|
|
18
|
+
'locale/en.json',
|
|
19
|
+
'locale/zh.json',
|
|
20
|
+
'ui-bundle/index.js',
|
|
21
|
+
'ui-bundle/package.json',
|
|
22
|
+
'ui-bundle/cordis.patch.yml',
|
|
23
|
+
'COMPATIBILITY.md',
|
|
24
|
+
]
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const output = execFileSync(npm, ['pack', '--dry-run', '--json'], {
|
|
16
|
-
cwd: root,
|
|
17
|
-
encoding: 'utf8',
|
|
18
|
-
shell: process.platform === 'win32',
|
|
19
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
20
|
-
})
|
|
21
|
-
parsed = JSON.parse(output)
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error(`pack check: npm pack failed: ${String(error?.message ?? error)}`)
|
|
24
|
-
process.exit(1)
|
|
26
|
+
function requiredMissing(paths, required = REQUIRED_FILES) {
|
|
27
|
+
return required.filter((file) => !paths.includes(file))
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
function main() {
|
|
31
|
+
const rootArg = process.argv.indexOf('--root')
|
|
32
|
+
const root = rootArg > 0 ? path.resolve(process.argv[rootArg + 1]) : ROOT
|
|
33
|
+
const manifestFile = path.join(root, 'scripts', 'pack-manifest.txt')
|
|
34
|
+
const write = process.argv.includes('--write')
|
|
35
|
+
|
|
36
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
37
|
+
let parsed
|
|
38
|
+
try {
|
|
39
|
+
const output = execFileSync(npm, ['pack', '--dry-run', '--json'], {
|
|
40
|
+
cwd: root,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
shell: process.platform === 'win32',
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
})
|
|
45
|
+
parsed = JSON.parse(output)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`pack check: npm pack failed: ${String(error?.message ?? error)}`)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entry = Array.isArray(parsed) ? parsed[0] : parsed
|
|
52
|
+
const paths = (entry?.files ?? []).map((file) => file.path).filter(Boolean).sort()
|
|
53
|
+
if (write) {
|
|
54
|
+
fs.writeFileSync(manifestFile, `${paths.join('\n')}\n`)
|
|
55
|
+
console.log(`pack check: wrote ${paths.length} paths to ${path.relative(root, manifestFile)}`)
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
const expected = fs.readFileSync(manifestFile, 'utf8').split('\n').filter(Boolean).sort()
|
|
59
|
+
const added = paths.filter((file) => !expected.includes(file))
|
|
60
|
+
const removed = expected.filter((file) => !paths.includes(file))
|
|
61
|
+
if (added.length > 0 || removed.length > 0) {
|
|
62
|
+
console.error('pack check: published file list changed; review and update scripts/pack-manifest.txt')
|
|
63
|
+
if (added.length > 0) console.error(` added: ${added.join(', ')}`)
|
|
64
|
+
if (removed.length > 0) console.error(` removed: ${removed.join(', ')}`)
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
const missing = requiredMissing(paths)
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
console.error(`pack check: required runtime files missing: ${missing.join(', ')}`)
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
console.log(`pack check: ${paths.length} files match scripts/pack-manifest.txt`)
|
|
48
73
|
}
|
|
49
|
-
|
|
74
|
+
|
|
75
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
76
|
+
if (isMain) main()
|
|
77
|
+
|
|
78
|
+
export { REQUIRED_FILES, requiredMissing }
|