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,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression closure audit (#85): a closed `regression` issue must name a
|
|
3
|
+
// `Regression test:` path that exists.
|
|
4
|
+
//
|
|
5
|
+
// A regression is "worked in a released version, then stopped". The project's
|
|
6
|
+
// own closure standard is a pointer, not prose: the incident template
|
|
7
|
+
// (docs/operations/incident.md) carries six fields and the regression test path
|
|
8
|
+
// is the one that can be checked. This script reads a local tracker export, the
|
|
9
|
+
// same format as `npm run audit:issues`, so it never touches the network:
|
|
10
|
+
//
|
|
11
|
+
// { "issues": [ { number, title, state, labels, closedAt, body, comments } ] }
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node scripts/regression-audit.mjs [--tracker <file>] [--root <dir>] [--json]
|
|
15
|
+
//
|
|
16
|
+
// Exit codes: 0 all checked regressions point at an existing test; 1 at least
|
|
17
|
+
// one is missing or points at a nonexistent/non-test path; 2 the export is
|
|
18
|
+
// missing or malformed.
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { inspectPointer, isClosed, labelsOf, parseTracker } from './issue-audit.mjs'
|
|
23
|
+
import { findingRecorder, parseArgs, runMain } from './lib/cli.mjs'
|
|
24
|
+
|
|
25
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
+
const DEFAULT_TRACKER = path.join('tracker', 'issues.json')
|
|
27
|
+
const USAGE = `usage: node scripts/regression-audit.mjs [--tracker <file>] [--root <dir>] [--json]
|
|
28
|
+
|
|
29
|
+
--tracker local tracker export (default: tracker/issues.json)
|
|
30
|
+
--root repository the Regression test paths resolve against (default: this package)
|
|
31
|
+
--json print the report as JSON
|
|
32
|
+
--help print this text
|
|
33
|
+
|
|
34
|
+
A regression issue is closed when state is closed (or closedAt is set) and its
|
|
35
|
+
labels include "regression". Its body or newest comment must carry:
|
|
36
|
+
|
|
37
|
+
Regression test: tests/<file>.test.mjs
|
|
38
|
+
|
|
39
|
+
The path must exist under --root and must be a test file. Produce the export once
|
|
40
|
+
with gh (see CONTRIBUTING.md, "Regressions and incidents"); the audit itself is
|
|
41
|
+
offline.`
|
|
42
|
+
|
|
43
|
+
function cleanValue(value) {
|
|
44
|
+
return String(value)
|
|
45
|
+
.trim()
|
|
46
|
+
.replace(/^`+|`+$/g, '')
|
|
47
|
+
.trim()
|
|
48
|
+
.replace(/^["']+|["']+$/g, '')
|
|
49
|
+
.trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The newest text (body first, then comments oldest to newest) that carries a
|
|
54
|
+
* `Regression test:` line is the closure evidence. Returns `{ pointer }` or
|
|
55
|
+
* `{ error: 'missing' }`.
|
|
56
|
+
* @param {any} issue
|
|
57
|
+
*/
|
|
58
|
+
export function selectRegressionField(issue) {
|
|
59
|
+
const sources = []
|
|
60
|
+
if (typeof issue.body === 'string' && issue.body.trim()) sources.push(issue.body)
|
|
61
|
+
const comments = Array.isArray(issue.comments) ? issue.comments : []
|
|
62
|
+
for (const comment of comments) {
|
|
63
|
+
const body = typeof comment === 'string' ? comment : comment?.body
|
|
64
|
+
if (typeof body === 'string' && body.trim()) sources.push(body)
|
|
65
|
+
}
|
|
66
|
+
for (let index = sources.length - 1; index >= 0; index -= 1) {
|
|
67
|
+
for (const rawLine of sources[index].split(/\r?\n/)) {
|
|
68
|
+
const line = rawLine.replace(/^\s*(?:[-*+]\s+|>\s*)+/, '')
|
|
69
|
+
const match = /^Regression test\s*:\s*(.+)$/i.exec(line)
|
|
70
|
+
if (!match) continue
|
|
71
|
+
const pointer = cleanValue(match[1])
|
|
72
|
+
if (pointer) return { pointer }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { error: 'missing' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Audit closed regression issues against the repository test tree.
|
|
80
|
+
* @param {any[]} issues parsed tracker issues
|
|
81
|
+
* @param {{ root?: string }} [options]
|
|
82
|
+
*/
|
|
83
|
+
export function auditRegressions(issues, options = {}) {
|
|
84
|
+
const root = path.resolve(options.root ?? PACKAGE_ROOT)
|
|
85
|
+
const findings = []
|
|
86
|
+
let checked = 0
|
|
87
|
+
for (const issue of issues) {
|
|
88
|
+
if (!isClosed(issue)) continue
|
|
89
|
+
const labels = labelsOf(issue).map((label) => label.toLowerCase())
|
|
90
|
+
if (!labels.includes('regression')) continue
|
|
91
|
+
checked += 1
|
|
92
|
+
const label = `#${issue.number} ${String(issue.title ?? '').trim()}`
|
|
93
|
+
const record = findingRecorder(findings, { number: issue.number, title: issue.title })
|
|
94
|
+
const field = selectRegressionField(issue)
|
|
95
|
+
if (field.error) {
|
|
96
|
+
record(
|
|
97
|
+
'missing-regression-test',
|
|
98
|
+
'error',
|
|
99
|
+
`${label}: closed regression has no "Regression test:" field (see docs/operations/incident.md)`,
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const problem = inspectPointer(field.pointer, root)
|
|
104
|
+
if (!problem) continue
|
|
105
|
+
if (problem.code === 'missing-test') {
|
|
106
|
+
record('missing-regression-test', 'error', `${label}: Regression test names a missing file: ${field.pointer}`)
|
|
107
|
+
} else if (problem.code === 'not-a-test') {
|
|
108
|
+
record('not-a-test', 'error', `${label}: Regression test is not a test file: ${field.pointer} (${problem.detail})`)
|
|
109
|
+
} else {
|
|
110
|
+
record(
|
|
111
|
+
'invalid-regression-test',
|
|
112
|
+
'error',
|
|
113
|
+
`${label}: Regression test must be a repository-relative path: ${field.pointer} (${problem.detail})`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const errors = findings.filter((item) => item.level === 'error')
|
|
118
|
+
return { ok: errors.length === 0, checked, findings, errors: errors.length }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function main(argv) {
|
|
122
|
+
const options = parseArgs(argv, {
|
|
123
|
+
initial: { tracker: null, root: PACKAGE_ROOT, json: false, help: false },
|
|
124
|
+
values: {
|
|
125
|
+
'--tracker': 'tracker',
|
|
126
|
+
'--root': { key: 'root', parse: path.resolve },
|
|
127
|
+
},
|
|
128
|
+
flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
|
|
129
|
+
})
|
|
130
|
+
if (options.help) {
|
|
131
|
+
console.log(USAGE)
|
|
132
|
+
return 0
|
|
133
|
+
}
|
|
134
|
+
const tracker = options.tracker ? path.resolve(process.cwd(), options.tracker) : path.join(PACKAGE_ROOT, DEFAULT_TRACKER)
|
|
135
|
+
let text
|
|
136
|
+
try {
|
|
137
|
+
text = fs.readFileSync(tracker, 'utf8')
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(`regression-audit: cannot read tracker export ${tracker}: ${error?.message ?? error}`)
|
|
140
|
+
console.error('regression-audit: produce one with the gh command in CONTRIBUTING.md ("Regressions and incidents")')
|
|
141
|
+
return 2
|
|
142
|
+
}
|
|
143
|
+
let issues
|
|
144
|
+
let result
|
|
145
|
+
try {
|
|
146
|
+
issues = parseTracker(text)
|
|
147
|
+
result = auditRegressions(issues, { root: options.root })
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`regression-audit: ${error.message}`)
|
|
150
|
+
return 2
|
|
151
|
+
}
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify({ tracker, root: path.resolve(options.root), ...result }, null, 2))
|
|
154
|
+
return result.ok ? 0 : 1
|
|
155
|
+
}
|
|
156
|
+
console.log(`regression-audit: ${tracker}`)
|
|
157
|
+
console.log(`regression-audit: ${result.checked} closed regression(s) checked, ${result.errors} error(s)`)
|
|
158
|
+
for (const item of result.findings) console.log(`${item.level} [${item.code}] ${item.message}`)
|
|
159
|
+
if (result.ok) console.log('regression-audit: every closed regression names an existing test file')
|
|
160
|
+
else console.log('regression-audit: a closed regression has no verifiable test pointer; see docs/operations/incident.md')
|
|
161
|
+
return result.ok ? 0 : 1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'regression-audit', usage: USAGE })
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import fs from 'node:fs'
|
|
28
28
|
import path from 'node:path'
|
|
29
29
|
import { spawnSync } from 'node:child_process'
|
|
30
|
-
import { compareVersions } from '../
|
|
30
|
+
import { compareVersions } from '../host/compatibility.js'
|
|
31
31
|
import { findingRecorder, parseArgs, runMain, UsageError } from './lib/cli.mjs'
|
|
32
32
|
|
|
33
33
|
const USAGE = `usage: node scripts/release-check.mjs [--mode pr|tag] [--tag <name>] [--json] [--help]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"date": "2026-10-23",
|
|
4
|
+
"surfaces": {
|
|
5
|
+
"configKeys": {
|
|
6
|
+
"seed": 64,
|
|
7
|
+
"value": 56,
|
|
8
|
+
"decisions": []
|
|
9
|
+
},
|
|
10
|
+
"sections": {
|
|
11
|
+
"seed": 15,
|
|
12
|
+
"value": 14,
|
|
13
|
+
"decisions": []
|
|
14
|
+
},
|
|
15
|
+
"tools": {
|
|
16
|
+
"seed": 27,
|
|
17
|
+
"value": 29,
|
|
18
|
+
"decisions": [
|
|
19
|
+
{
|
|
20
|
+
"from": 27,
|
|
21
|
+
"to": 29,
|
|
22
|
+
"date": "2026-10-23",
|
|
23
|
+
"correction": {
|
|
24
|
+
"reason": "Measurement correction, not surface growth: the 2.3.0 baseline recorded only define()-registered tools. tower_mode and tower_control are registered directly by index.js, so the shipped surface was always 29 (#118). The recorder in host/tool-registry.js now covers both paths.",
|
|
25
|
+
"date": "2026-10-23",
|
|
26
|
+
"ref": "docs/adr/0004-surface-budget.md"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"components": {
|
|
32
|
+
"seed": 4,
|
|
33
|
+
"value": 8,
|
|
34
|
+
"decisions": [
|
|
35
|
+
{
|
|
36
|
+
"from": 4,
|
|
37
|
+
"to": 1,
|
|
38
|
+
"date": "2026-10-23",
|
|
39
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
40
|
+
"note": "The component surface joins the budget with its history. 2.0.4 shipped four top-level rows (tower, tower-skills, preset-tower-worker, preset-tower-reviewer); 2.3.0 replaced them with one tower-bundle group row that still mounts the same four sections. This is a recorded contraction, not a free ratchet: the seed stays at 4 so any further move -- including the #121 growth back to individually toggleable optional rows -- must extend this chain with a dated decision."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"from": 1,
|
|
44
|
+
"to": 8,
|
|
45
|
+
"date": "2026-10-23",
|
|
46
|
+
"adr": "docs/adr/0005-component-surface.md",
|
|
47
|
+
"note": "#121 restores the #65 policy from ADR 0005: the one tower-bundle group becomes a mandatory tower host anchor plus seven individually switchable optional rows (tower-skills, tower-batch, tower-import, tower-leases, tower-tests, tower-hmr shipped off, preset-tower-reviewer conditional). The host anchor and preset-tower-worker stay mandatory; tower_doctor and tower_recover stay mandatory with the host (#122), so they are not rows. configKeys and sections shrank in the same wave because the seven tools.expose* keys and dev.hotReload became rows."
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Concept-budget gate (#84): compare the live surface with the committed
|
|
3
|
+
// baseline and require evidence for growth.
|
|
4
|
+
//
|
|
5
|
+
// The surface is four counts the user has to hold at once:
|
|
6
|
+
// config keys leaf keys in domain/config/schema.js (one level deep)
|
|
7
|
+
// sections object groups in that schema
|
|
8
|
+
// tools every tool the Host entry registers, including the
|
|
9
|
+
// tower_mode/tower_control pair that index.js registers
|
|
10
|
+
// directly rather than through define() (#118)
|
|
11
|
+
// components user-visible rows in cordis.patch.yml (#120), so the 2.3.0
|
|
12
|
+
// 4 -> 1 contraction and any future move need a written decision
|
|
13
|
+
//
|
|
14
|
+
// `scripts/surface-baseline.json` freezes those counts. A PR that grows a count
|
|
15
|
+
// fails here until the baseline carries a decision with either an ADR under
|
|
16
|
+
// docs/adr/, deletions that no longer exist in the live surface, or an explicit
|
|
17
|
+
// measurement-correction record (reason + date). The same file supplies the date
|
|
18
|
+
// and counts printed in the generated config/tools reference headers, so growth
|
|
19
|
+
// is visible in a diff.
|
|
20
|
+
//
|
|
21
|
+
// Usage:
|
|
22
|
+
// node scripts/surface-baseline.mjs [--root <dir>] [--json]
|
|
23
|
+
import fs from 'node:fs'
|
|
24
|
+
import path from 'node:path'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
import { configSurface } from '../domain/config/schema.js'
|
|
27
|
+
import { parseCordisPatch } from '../domain/components.js'
|
|
28
|
+
import { SURFACE_CLASSIFICATION_FILE, SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
|
|
29
|
+
import { loadShippedToolSurface } from './lib/tool-surface.mjs'
|
|
30
|
+
import { checkSurfaceBudget } from './lib/surface.mjs'
|
|
31
|
+
import { parseArgs, runMain } from './lib/cli.mjs'
|
|
32
|
+
|
|
33
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
34
|
+
const USAGE = `usage: node scripts/surface-baseline.mjs [--root <dir>] [--json]
|
|
35
|
+
|
|
36
|
+
--root repository root to check (default: this package)
|
|
37
|
+
--json print the report as JSON
|
|
38
|
+
--help print this text
|
|
39
|
+
|
|
40
|
+
Growth is refused by default. To grow a count, update ${SURFACE_BASELINE_FILE}
|
|
41
|
+
with a decision that names an existing docs/adr/ ADR, deletions that are no
|
|
42
|
+
longer in the live surface, or a measurement-correction record with a reason and
|
|
43
|
+
date. The classification table in
|
|
44
|
+
${SURFACE_CLASSIFICATION_FILE} must cover every key and tool exactly once, and
|
|
45
|
+
the component count comes from the visible rows in cordis.patch.yml.`
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The live tool names: apply the shipped Host entry so every registration path
|
|
49
|
+
* is included, then release the mock context's timers.
|
|
50
|
+
*/
|
|
51
|
+
function liveToolNames() {
|
|
52
|
+
const surface = loadShippedToolSurface()
|
|
53
|
+
try {
|
|
54
|
+
return surface.names
|
|
55
|
+
} finally {
|
|
56
|
+
surface.dispose()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The live surface used by the gate. Exported so tests can compare the counts
|
|
62
|
+
* with the generated references without spawning the CLI.
|
|
63
|
+
* @param {string} [root] repository root (defaults to this package)
|
|
64
|
+
*/
|
|
65
|
+
export function liveSurface(root = PACKAGE_ROOT) {
|
|
66
|
+
const config = configSurface()
|
|
67
|
+
const tools = liveToolNames()
|
|
68
|
+
const patch = parseCordisPatch(fs.readFileSync(path.join(root, 'cordis.patch.yml'), 'utf8'))
|
|
69
|
+
return {
|
|
70
|
+
configKeys: config.count,
|
|
71
|
+
sections: config.sections.length,
|
|
72
|
+
tools: tools.length,
|
|
73
|
+
components: patch.visible.length,
|
|
74
|
+
liveNames: {
|
|
75
|
+
configKeys: config.keys,
|
|
76
|
+
sections: config.sections,
|
|
77
|
+
tools,
|
|
78
|
+
components: patch.visible,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main(argv) {
|
|
84
|
+
const options = parseArgs(argv, {
|
|
85
|
+
initial: { root: PACKAGE_ROOT, json: false, help: false },
|
|
86
|
+
values: { '--root': { key: 'root', parse: path.resolve } },
|
|
87
|
+
flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
|
|
88
|
+
})
|
|
89
|
+
if (options.help) {
|
|
90
|
+
console.log(USAGE)
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
const root = path.resolve(options.root)
|
|
94
|
+
if (!fs.existsSync(path.join(root, SURFACE_BASELINE_FILE))) {
|
|
95
|
+
console.error(`surface: ${SURFACE_BASELINE_FILE} is missing under ${root}`)
|
|
96
|
+
return 2
|
|
97
|
+
}
|
|
98
|
+
const current = liveSurface()
|
|
99
|
+
const result = checkSurfaceBudget({ root, current })
|
|
100
|
+
if (options.json) {
|
|
101
|
+
console.log(JSON.stringify({ root, current, ...result }, null, 2))
|
|
102
|
+
return result.ok ? 0 : 1
|
|
103
|
+
}
|
|
104
|
+
console.log(`surface: ${surfaceLine(current)}`)
|
|
105
|
+
console.log(`surface: components: ${current.components} visible (${current.liveNames.components.join(', ')})`)
|
|
106
|
+
if (result.baseline) console.log(`surface: baseline ${result.baseline.date} (${SURFACE_BASELINE_FILE})`)
|
|
107
|
+
if (result.classification) {
|
|
108
|
+
const configRows = result.classification.entries.filter((entry) => entry.kind === 'config').length
|
|
109
|
+
const toolRows = result.classification.entries.filter((entry) => entry.kind === 'tools').length
|
|
110
|
+
console.log(`surface: classified ${configRows} config keys and ${toolRows} tools (${SURFACE_CLASSIFICATION_FILE})`)
|
|
111
|
+
}
|
|
112
|
+
for (const finding of result.findings) console.log(`surface: ${finding.level} [${finding.code}] ${finding.message}`)
|
|
113
|
+
if (result.ok) console.log('surface: ok — counts match the committed baseline and every entry is classified')
|
|
114
|
+
else console.log(`surface: ${result.findings.length} problem(s); growth needs an ADR or a matching deletion`)
|
|
115
|
+
return result.ok ? 0 : 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'surface', usage: USAGE })
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Type-coverage fitness rule (#115, step 1).
|
|
2
|
+
//
|
|
3
|
+
// The checked program must be derived from the shipped program: every runtime
|
|
4
|
+
// `.js` file that `package.json` publishes has to appear in the `tsc` program,
|
|
5
|
+
// otherwise "this file is unchecked" cannot even be reported. Today the
|
|
6
|
+
// tsconfig `include` list is hand-written, so a new module outside it is
|
|
7
|
+
// invisible by construction; this gate makes that gap a red check.
|
|
8
|
+
//
|
|
9
|
+
// Run: node scripts/types-coverage.mjs [--root <dir>] [--json]
|
|
10
|
+
//
|
|
11
|
+
// The shipped set comes from `package.json` `files` (the same list `npm pack`
|
|
12
|
+
// uses; scripts/pack-manifest.txt pins the reviewed snapshot). The checked set
|
|
13
|
+
// comes from `tsc --noEmit --listFiles`, so no second enumeration is involved:
|
|
14
|
+
// changing the tsconfig is enough and this gate follows.
|
|
15
|
+
//
|
|
16
|
+
// Excluded tiers are data with a reason, not silent gaps:
|
|
17
|
+
// - `ui-bundle/**` is the optional Web client: a browser artifact with DOM
|
|
18
|
+
// types and no Node runtime, checked by tests/ui-bundle.test.mjs and the
|
|
19
|
+
// architecture client rules instead of the Node tsc program;
|
|
20
|
+
// - `scripts/**` is build/dev tooling (ESM `.mjs`, not plugin runtime).
|
|
21
|
+
import { execFileSync } from 'node:child_process'
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import path from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const SELF_ROOT = path.resolve(MODULE_DIR, '..')
|
|
28
|
+
const TSC_BIN = path.join(SELF_ROOT, 'node_modules', 'typescript', 'bin', 'tsc')
|
|
29
|
+
const WALK_SKIP = new Set(['node_modules', '.git', '.dsh-tower-worktrees', 'coverage', '.nyc_output'])
|
|
30
|
+
|
|
31
|
+
/** Shipped `.js` tiers that are deliberately outside the Node tsc program. */
|
|
32
|
+
export const NOT_CHECKED_TIERS = [
|
|
33
|
+
{
|
|
34
|
+
pattern: 'ui-bundle/**',
|
|
35
|
+
reason: 'optional Web client: browser artifact, checked by tests/ui-bundle.test.mjs and the client architecture rules',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: 'scripts/**',
|
|
39
|
+
reason: 'build/dev tooling, not plugin runtime',
|
|
40
|
+
},
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/** Expand one package.json `files` pattern into repo-relative paths. */
|
|
44
|
+
export function globToRegExp(pattern) {
|
|
45
|
+
const source = String(pattern)
|
|
46
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
47
|
+
.replace(/\*\*/g, '\u0000')
|
|
48
|
+
.replace(/\*/g, '[^/]*')
|
|
49
|
+
.replace(/\?/g, '[^/]')
|
|
50
|
+
.replace(/\u0000\//g, '(?:[^/]+/)*')
|
|
51
|
+
.replace(/\u0000/g, '.*')
|
|
52
|
+
return new RegExp(`^${source}$`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function walkFiles(root, relative = '') {
|
|
56
|
+
const dir = path.join(root, relative)
|
|
57
|
+
if (!fs.existsSync(dir)) return []
|
|
58
|
+
const out = []
|
|
59
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
if (WALK_SKIP.has(entry.name)) continue
|
|
61
|
+
const next = relative ? `${relative}/${entry.name}` : entry.name
|
|
62
|
+
if (entry.isDirectory()) out.push(...walkFiles(root, next))
|
|
63
|
+
else out.push(next)
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve `package.json` `files` into the repo-relative file list.
|
|
70
|
+
* A trailing slash (or a bare directory name) means the whole directory, the
|
|
71
|
+
* same reading `npm pack` gives it.
|
|
72
|
+
* @param {string[]} patterns
|
|
73
|
+
* @param {string[]} files every repo-relative file
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
export function expandShipped(patterns, files) {
|
|
77
|
+
const out = new Set()
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
if (typeof pattern !== 'string' || pattern.length === 0) continue
|
|
80
|
+
const normalized = pattern.endsWith('/') ? `${pattern}**` : pattern
|
|
81
|
+
const matcher = globToRegExp(normalized)
|
|
82
|
+
const directory = !/\*|\?/.test(normalized) && files.some((file) => file.startsWith(`${normalized}/`))
|
|
83
|
+
const hit = (file) => (directory ? file === normalized || file.startsWith(`${normalized}/`) : matcher.test(file))
|
|
84
|
+
for (const file of files) if (hit(file)) out.add(file)
|
|
85
|
+
}
|
|
86
|
+
return [...out].sort()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The Node-runtime slice of the shipped set: `.js` files minus the documented
|
|
91
|
+
* non-Node tiers.
|
|
92
|
+
* @param {string[]} shipped
|
|
93
|
+
* @returns {{ runtime: string[], excluded: Array<{ file: string, reason: string }> }}
|
|
94
|
+
*/
|
|
95
|
+
export function runtimeJavaScript(shipped) {
|
|
96
|
+
const excluded = []
|
|
97
|
+
const runtime = []
|
|
98
|
+
for (const file of shipped) {
|
|
99
|
+
if (!file.endsWith('.js')) continue
|
|
100
|
+
const tier = NOT_CHECKED_TIERS.find((entry) => globToRegExp(entry.pattern).test(file))
|
|
101
|
+
if (tier) excluded.push({ file, reason: tier.reason })
|
|
102
|
+
else runtime.push(file)
|
|
103
|
+
}
|
|
104
|
+
return { runtime: runtime.sort(), excluded }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compare the shipped runtime set with the checked program. One direction only:
|
|
109
|
+
* a shipped file outside the program fails. Extra program files (tests/*.ts,
|
|
110
|
+
* the declaration files, fixture helpers) are expected and allowed.
|
|
111
|
+
* @param {{ runtime: string[], program: string[] }} input
|
|
112
|
+
* @returns {{ ok: boolean, missing: string[], checked: number, total: number }}
|
|
113
|
+
*/
|
|
114
|
+
export function evaluateCoverage({ runtime, program }) {
|
|
115
|
+
const checked = new Set(program)
|
|
116
|
+
const missing = runtime.filter((file) => !checked.has(file))
|
|
117
|
+
return { ok: missing.length === 0, missing, checked: runtime.length - missing.length, total: runtime.length }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** List the project files (never node_modules) in the `tsc` program. */
|
|
121
|
+
function programFiles(root) {
|
|
122
|
+
const config = path.join(root, 'tsconfig.json')
|
|
123
|
+
if (!fs.existsSync(config)) throw new Error(`no tsconfig.json under ${root}`)
|
|
124
|
+
if (!fs.existsSync(TSC_BIN)) throw new Error('typescript is not installed; run npm ci first')
|
|
125
|
+
let output
|
|
126
|
+
try {
|
|
127
|
+
output = execFileSync(process.execPath, [TSC_BIN, '--noEmit', '--listFiles', '-p', config], {
|
|
128
|
+
cwd: root,
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
131
|
+
})
|
|
132
|
+
} catch (error) {
|
|
133
|
+
// A program with type errors still lists its files; only a config failure
|
|
134
|
+
// (no input at all) is fatal for this gate. Keep diagnostics out of the
|
|
135
|
+
// parse by only accepting existing absolute paths.
|
|
136
|
+
output = String(error?.stdout ?? '')
|
|
137
|
+
if (!output.trim()) throw new Error(`tsc --listFiles failed: ${String(error?.stderr ?? error?.message ?? error)}`)
|
|
138
|
+
}
|
|
139
|
+
const files = new Set()
|
|
140
|
+
for (const raw of output.split('\n')) {
|
|
141
|
+
const line = raw.trim()
|
|
142
|
+
if (!line || !path.isAbsolute(line)) continue
|
|
143
|
+
if (!fs.existsSync(line)) continue
|
|
144
|
+
const relative = path.relative(root, line).split(path.sep).join('/')
|
|
145
|
+
if (!relative || relative.startsWith('..') || relative.startsWith('node_modules/')) continue
|
|
146
|
+
if (relative.includes('/node_modules/')) continue
|
|
147
|
+
files.add(relative)
|
|
148
|
+
}
|
|
149
|
+
return [...files].sort()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function main() {
|
|
153
|
+
const rootArg = process.argv.indexOf('--root')
|
|
154
|
+
const root = rootArg > 0 ? path.resolve(process.argv[rootArg + 1]) : SELF_ROOT
|
|
155
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
156
|
+
const shipped = expandShipped(Array.isArray(pkg.files) ? pkg.files : [], walkFiles(root))
|
|
157
|
+
const { runtime, excluded } = runtimeJavaScript(shipped)
|
|
158
|
+
let program
|
|
159
|
+
try {
|
|
160
|
+
program = programFiles(root)
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(`types coverage: ${String(error?.message ?? error)}`)
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
const result = evaluateCoverage({ runtime, program })
|
|
166
|
+
if (process.argv.includes('--json')) {
|
|
167
|
+
console.log(JSON.stringify({ ...result, program: program.length, excluded }, null, 2))
|
|
168
|
+
} else if (result.ok) {
|
|
169
|
+
console.log(
|
|
170
|
+
`types coverage: ${result.checked}/${result.total} shipped runtime .js files are in the tsc program` +
|
|
171
|
+
` (${program.length} program files, ${excluded.length} excluded: ${[...new Set(excluded.map((entry) => entry.file.split('/')[0]))].join(', ') || 'none'})`,
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
console.error(
|
|
175
|
+
`types coverage: ${result.missing.length} shipped runtime .js file(s) are outside the tsc program:`,
|
|
176
|
+
)
|
|
177
|
+
for (const file of result.missing) console.error(` - ${file}`)
|
|
178
|
+
console.error('add the file (or its directory glob) to tsconfig.json `include`')
|
|
179
|
+
}
|
|
180
|
+
process.exit(result.ok ? 0 : 1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
184
|
+
if (isMain) main()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
domain/contracts/outputs.js
|
|
2
|
+
domain/contracts/tool-inputs/collaboration.js
|
|
3
|
+
domain/contracts/tool-inputs/findings.js
|
|
4
|
+
domain/contracts/tool-inputs/index.js
|
|
5
|
+
domain/contracts/tool-inputs/integration.js
|
|
6
|
+
domain/contracts/tool-inputs/missions.js
|
|
7
|
+
domain/contracts/tool-inputs/ops.js
|
|
8
|
+
domain/contracts/tool-inputs/shared.js
|
|
9
|
+
types/cordis.d.ts
|
|
10
|
+
types/index.d.ts
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Strict type ratchet (#115, step 4).
|
|
2
|
+
//
|
|
3
|
+
// `tsconfig.strict.json` turns `noImplicitAny: true` on for an explicit,
|
|
4
|
+
// growing file list. The list is the ratchet: CI runs the strict config, and
|
|
5
|
+
// this gate makes the list a reviewed artifact instead of a file that can
|
|
6
|
+
// silently shrink.
|
|
7
|
+
//
|
|
8
|
+
// Run: node scripts/types-strict.mjs [--write] [--allow-shrink] [--root <dir>]
|
|
9
|
+
//
|
|
10
|
+
// * no flag — check: every listed file exists, every file in the
|
|
11
|
+
// seed tier (`domain/contracts/**`) is listed, and the
|
|
12
|
+
// reviewed snapshot (scripts/types-strict-manifest.txt)
|
|
13
|
+
// matches the list exactly.
|
|
14
|
+
// * --write — record the current list in the snapshot. Removing an
|
|
15
|
+
// entry additionally needs `--allow-shrink`, so shrinking
|
|
16
|
+
// the ratchet is a deliberate, visible decision.
|
|
17
|
+
//
|
|
18
|
+
// The list must stay explicit files: a glob would let a new file enter the
|
|
19
|
+
// strict program without a reviewed one-line diff, which is the opposite of a
|
|
20
|
+
// ratchet. `domain/contracts/**` is the seed tier: adding a file there fails
|
|
21
|
+
// the check until the list and the snapshot name it.
|
|
22
|
+
import fs from 'node:fs'
|
|
23
|
+
import path from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
27
|
+
export const MANIFEST = 'scripts/types-strict-manifest.txt'
|
|
28
|
+
const SEED_DIRECTORIES = ['domain/contracts']
|
|
29
|
+
|
|
30
|
+
function walk(root, relative) {
|
|
31
|
+
const dir = path.join(root, relative)
|
|
32
|
+
if (!fs.existsSync(dir)) return []
|
|
33
|
+
const out = []
|
|
34
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
const next = `${relative}/${entry.name}`
|
|
36
|
+
if (entry.isDirectory()) out.push(...walk(root, next))
|
|
37
|
+
else out.push(next)
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Entries that are not repo-relative explicit files. @param {unknown} include */
|
|
43
|
+
export function invalidEntries(include) {
|
|
44
|
+
if (!Array.isArray(include) || include.length === 0) return ['include must be a non-empty array of files']
|
|
45
|
+
const problems = []
|
|
46
|
+
for (const entry of include) {
|
|
47
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
48
|
+
problems.push(`${JSON.stringify(entry)} is not a non-empty path`)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (path.isAbsolute(entry) || entry.includes('\\') || entry.split('/').includes('..')) {
|
|
52
|
+
problems.push(`${entry}: use a repo-relative path with forward slashes`)
|
|
53
|
+
} else if (/[*?]/.test(entry)) {
|
|
54
|
+
problems.push(`${entry}: the strict list must name explicit files, not a glob`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return problems
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @param {{ include: string[], manifest: string[] }} input */
|
|
61
|
+
export function compareLists({ include, manifest }) {
|
|
62
|
+
const listed = new Set(include)
|
|
63
|
+
const pinned = new Set(manifest)
|
|
64
|
+
return {
|
|
65
|
+
lost: manifest.filter((file) => !listed.has(file)).sort(),
|
|
66
|
+
grew: include.filter((file) => !pinned.has(file)).sort(),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every seed-tier file the strict list must name. */
|
|
71
|
+
function seedFiles(root) {
|
|
72
|
+
return SEED_DIRECTORIES.flatMap((directory) => walk(root, directory)).filter((file) => file.endsWith('.js')).sort()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readManifest(root) {
|
|
76
|
+
const file = path.join(root, MANIFEST)
|
|
77
|
+
if (!fs.existsSync(file)) return []
|
|
78
|
+
return fs.readFileSync(file, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fail(lines) {
|
|
82
|
+
console.error('types strict: the ratchet would shrink or drift:')
|
|
83
|
+
for (const line of lines) console.error(` - ${line}`)
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function main() {
|
|
88
|
+
const rootArg = process.argv.indexOf('--root')
|
|
89
|
+
const root = rootArg > 0 ? path.resolve(process.argv[rootArg + 1]) : SELF_ROOT
|
|
90
|
+
const write = process.argv.includes('--write')
|
|
91
|
+
const allowShrink = process.argv.includes('--allow-shrink')
|
|
92
|
+
const configFile = path.join(root, 'tsconfig.strict.json')
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(configFile)) {
|
|
95
|
+
console.error(`types strict: ${path.relative(process.cwd(), configFile)} is missing`)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
}
|
|
98
|
+
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'))
|
|
99
|
+
if (config?.compilerOptions?.noImplicitAny !== true) {
|
|
100
|
+
console.error('types strict: tsconfig.strict.json must set compilerOptions.noImplicitAny: true explicitly')
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
const include = Array.isArray(config.include) ? config.include : []
|
|
104
|
+
const invalid = invalidEntries(include)
|
|
105
|
+
if (invalid.length > 0) fail(invalid)
|
|
106
|
+
|
|
107
|
+
const missingFiles = include.filter((file) => !fs.existsSync(path.join(root, file)))
|
|
108
|
+
if (missingFiles.length > 0) fail(missingFiles.map((file) => `${file}: listed in tsconfig.strict.json but does not exist`))
|
|
109
|
+
|
|
110
|
+
const seeds = seedFiles(root)
|
|
111
|
+
const unlistedSeeds = seeds.filter((file) => !include.includes(file))
|
|
112
|
+
if (unlistedSeeds.length > 0) {
|
|
113
|
+
fail(unlistedSeeds.map((file) => `${file}: in the seed tier but not in tsconfig.strict.json include`))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const manifest = readManifest(root)
|
|
117
|
+
const { lost, grew } = compareLists({ include, manifest })
|
|
118
|
+
if (write) {
|
|
119
|
+
if (lost.length > 0 && !allowShrink) {
|
|
120
|
+
fail([
|
|
121
|
+
...lost.map((file) => `${file}: pinned in ${MANIFEST} but missing from tsconfig.strict.json include`),
|
|
122
|
+
'pass --allow-shrink to record a deliberate removal',
|
|
123
|
+
])
|
|
124
|
+
}
|
|
125
|
+
fs.mkdirSync(path.join(root, 'scripts'), { recursive: true })
|
|
126
|
+
fs.writeFileSync(path.join(root, MANIFEST), `${[...include].sort().join('\n')}\n`)
|
|
127
|
+
console.log(`types strict: wrote ${include.length} files to ${MANIFEST} (${lost.length} removed, ${grew.length} added)`)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (lost.length > 0 || grew.length > 0) {
|
|
132
|
+
fail([
|
|
133
|
+
...lost.map((file) => `${file}: pinned in ${MANIFEST} but no longer checked (the ratchet must not shrink)`),
|
|
134
|
+
...grew.map((file) => `${file}: newly checked but not pinned; run npm run types:strict:write to record it`),
|
|
135
|
+
])
|
|
136
|
+
}
|
|
137
|
+
console.log(`types strict: ${include.length} files pinned, noImplicitAny over ${SEED_DIRECTORIES.join(', ')}/**`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
141
|
+
if (isMain) main()
|