dsh-tower 2.0.3 → 2.1.0-beta.1
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 +141 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +59 -16
- package/README.md +57 -16
- package/SECURITY.md +31 -1
- package/adapters/default.js +10 -0
- package/adapters/store-fs.js +6 -0
- package/adapters/vcs-git.js +4 -0
- package/app/batch.js +79 -0
- package/app/board.js +25 -0
- package/app/checkCompletion.js +76 -0
- package/app/enrichedMissions.js +26 -0
- package/app/findings.js +78 -0
- package/app/gate.js +108 -0
- package/app/init.js +249 -0
- package/app/merge.js +567 -0
- package/app/messages.js +66 -0
- package/app/mission.js +35 -0
- package/app/missionContext.js +14 -0
- package/app/plan.js +42 -0
- package/app/provision.js +193 -0
- package/app/read.js +57 -0
- package/app/rebase.js +302 -0
- package/app/recording.js +72 -0
- package/app/review.js +145 -0
- package/app/roster.js +123 -0
- package/app/spawn.js +125 -0
- package/app/teardown.js +184 -0
- package/app/tower.js +524 -0
- package/cordis.patch.yml +61 -53
- package/docs/README.md +33 -1
- package/docs/architecture/overview.md +114 -14
- package/docs/board.md +72 -0
- package/docs/examples/first-mission.md +2 -1
- package/docs/faq.md +11 -7
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +22 -6
- package/docs/guide/quickstart.md +12 -7
- package/docs/guide/release.md +125 -0
- package/docs/guide/scenarios-and-philosophy.md +86 -69
- package/docs/operations/security.md +65 -14
- package/docs/reference/blockers.md +11 -10
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +11 -7
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +10 -2
- package/docs/reference/events.md +7 -4
- package/docs/reference/testing.md +2 -1
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +8 -7
- package/docs/zh/README.md +4 -3
- package/docs/zh/faq.md +9 -6
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +17 -5
- package/docs/zh/guide/quickstart.md +13 -8
- package/docs/zh/guide/scenarios-and-philosophy.md +55 -50
- package/docs/zh/operations/security.md +43 -7
- package/docs/zh/reference/blockers.md +10 -10
- package/docs/zh/reference/config.md +11 -7
- package/docs/zh/reference/errors.md +4 -1
- package/docs/zh/reference/events.md +6 -3
- package/docs/zh/reference/testing.md +3 -2
- package/docs/zh/reference/tools.md +8 -7
- package/domain/collab/batch.js +112 -0
- package/domain/collab/messages.js +127 -0
- package/domain/collab/roster.js +158 -0
- package/domain/collab/spawn.js +188 -0
- package/domain/config/schema.js +404 -0
- package/domain/contracts/outputs.js +279 -0
- package/domain/contracts/tool-inputs/collaboration.js +58 -0
- package/domain/contracts/tool-inputs/findings.js +29 -0
- package/domain/contracts/tool-inputs/index.js +94 -0
- package/domain/contracts/tool-inputs/integration.js +37 -0
- package/domain/contracts/tool-inputs/missions.js +94 -0
- package/domain/contracts/tool-inputs/ops.js +75 -0
- package/domain/contracts/tool-inputs/shared.js +25 -0
- package/domain/errors.js +96 -0
- package/domain/gate.js +125 -0
- package/domain/integration/gate.js +176 -0
- package/domain/integration/init.js +225 -0
- package/domain/integration/liveness.js +31 -0
- package/domain/integration/merge.js +377 -0
- package/domain/integration/provision.js +124 -0
- package/domain/integration/rebase.js +236 -0
- package/domain/integration/review.js +52 -0
- package/domain/integration/teardown.js +118 -0
- package/domain/mission.js +68 -0
- package/domain/missions/completion.js +114 -0
- package/domain/missions/findings.js +201 -0
- package/domain/missions/lifecycle.js +185 -0
- package/domain/missions/plan.js +149 -0
- package/domain/missions/review.js +179 -0
- package/domain/planning.js +118 -0
- package/domain/read/enrichedMissions.js +58 -0
- package/domain/read/missionContext.js +53 -0
- package/domain/read/views.js +128 -0
- package/domain/report.js +59 -0
- package/domain/roster.js +40 -0
- package/domain/scope.js +105 -0
- package/domain/state/schema.js +233 -0
- package/domain/state/semantics.js +117 -0
- package/domain/state.js +33 -0
- package/domain/summary.js +114 -0
- package/domain/vocabulary.js +179 -0
- package/index.js +86 -34
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +54 -302
- package/lib/doctor.js +108 -9
- package/lib/git.js +93 -15
- package/lib/hmr.js +4 -3
- package/lib/leases.js +3 -2
- package/lib/legacy.js +20 -9
- package/lib/modeState.js +39 -0
- package/lib/modeStore.js +35 -33
- package/lib/persistence.js +349 -0
- package/lib/presets.js +35 -0
- package/lib/spawn-prechecks.js +168 -0
- package/lib/spawn-support.js +79 -0
- package/lib/store.js +57 -85
- package/lib/tests.js +46 -11
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +46 -22
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +33 -4
- package/ports/clock.js +6 -0
- package/ports/ids.js +7 -0
- package/ports/index.js +2 -0
- package/ports/store.js +11 -0
- package/ports/vcs.js +11 -0
- package/scripts/architecture.mjs +94 -0
- package/scripts/check-docs.mjs +44 -3
- package/scripts/check-pack.mjs +49 -0
- package/scripts/domain-metrics.mjs +48 -0
- package/scripts/generate-config-reference.mjs +46 -0
- package/scripts/generate-error-reference.mjs +29 -0
- package/scripts/generate-tools-reference.mjs +56 -0
- package/scripts/issue-audit.mjs +259 -0
- package/scripts/lib/cli.mjs +76 -0
- package/scripts/lib/tmp.mjs +20 -0
- package/scripts/mount-smoke/fixtures.mjs +162 -0
- package/scripts/mount-smoke/host.json +4 -0
- package/scripts/mount-smoke/probe.mjs +125 -0
- package/scripts/mount-smoke/surface.mjs +141 -0
- package/scripts/mount-smoke.mjs +270 -0
- package/scripts/pack-manifest.txt +203 -0
- package/scripts/release-check.mjs +303 -0
- package/skills/tower-recovery.md +7 -5
- package/tools/adopt.js +72 -0
- package/tools/batch.js +78 -0
- package/tools/complete.js +53 -0
- package/tools/diff.js +39 -0
- package/tools/doctor.js +50 -0
- package/tools/finding.js +40 -0
- package/tools/findings.js +39 -0
- package/tools/import-legacy.js +42 -0
- package/tools/inbox.js +39 -0
- package/tools/index.js +39 -0
- package/tools/init.js +63 -0
- package/tools/interrupt.js +42 -0
- package/tools/legacy-map.js +44 -0
- package/tools/merge.js +39 -0
- package/tools/mission.js +41 -0
- package/tools/plan.js +39 -0
- package/tools/preflight.js +44 -0
- package/tools/rebase.js +39 -0
- package/tools/recover.js +45 -0
- package/tools/reload.js +52 -0
- package/tools/resource.js +54 -0
- package/tools/review.js +45 -0
- package/tools/roster.js +60 -0
- package/tools/send.js +147 -0
- package/tools/spawn.js +196 -0
- package/tools/status.js +94 -0
- package/tools/teardown.js +42 -0
- package/tools/test.js +61 -0
- package/types/cordis.d.ts +6 -0
- package/types/index.d.ts +339 -0
- package/ui-bundle/README.md +111 -0
- package/ui-bundle/client.js +1980 -0
- package/ui-bundle/cordis.patch.yml +3 -0
- package/ui-bundle/icon.svg +6 -0
- package/ui-bundle/index.js +2 -0
- package/ui-bundle/locale/en.json +6 -0
- package/ui-bundle/locale/zh.json +6 -0
- package/ui-bundle/package.json +35 -0
- package/lib/errors.js +0 -111
- package/lib/tools.js +0 -2250
- package/lib/tower.js +0 -2689
- /package/{lib → domain}/batch.js +0 -0
package/lib/worktreeOwnership.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
// and must still be the Git worktree of the recorded branch in the recorded
|
|
7
7
|
// repository. Every mismatch is rejected with an actionable message; nothing
|
|
8
8
|
// is repaired or recreated here.
|
|
9
|
-
import fsp from '
|
|
9
|
+
import { fsp } from '../adapters/store-fs.js'
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
|
|
12
12
|
import { git } from './git.js'
|
|
13
13
|
|
|
14
14
|
function fail(code, message) {
|
|
15
|
+
/** @type {Error & {code?: string}} */
|
|
15
16
|
const error = new Error(message)
|
|
16
17
|
error.code = code
|
|
17
18
|
throw error
|
|
@@ -74,7 +75,7 @@ async function gitTop(resolved, label) {
|
|
|
74
75
|
* @param {string} [input.missionId] mission id, used in error messages
|
|
75
76
|
* @returns {Promise<{path:string, realPath:string}>}
|
|
76
77
|
*/
|
|
77
|
-
export async function assertOwnership(input = {}) {
|
|
78
|
+
export async function assertOwnership(input = /** @type {any} */ ({})) {
|
|
78
79
|
const storedPath = input?.storedPath
|
|
79
80
|
const storedRealPath = input?.storedRealPath
|
|
80
81
|
const branch = input?.branch
|
|
@@ -138,7 +139,7 @@ export async function assertOwnership(input = {}) {
|
|
|
138
139
|
} catch (error) {
|
|
139
140
|
fail('invalid-transition', `${label} ${resolved} has no readable HEAD: ${error.message}`)
|
|
140
141
|
}
|
|
141
|
-
const actualBranch = head === 'HEAD' ? null : head.trim()
|
|
142
|
+
const actualBranch = head === 'HEAD' ? null : String(head).trim()
|
|
142
143
|
if (actualBranch !== branch) {
|
|
143
144
|
fail(
|
|
144
145
|
'invalid-transition',
|
|
@@ -146,7 +147,7 @@ export async function assertOwnership(input = {}) {
|
|
|
146
147
|
)
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
let commonRaw
|
|
150
|
+
let commonRaw = ''
|
|
150
151
|
try {
|
|
151
152
|
commonRaw = await git(resolved, ['rev-parse', '--git-common-dir'])
|
|
152
153
|
} catch (error) {
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Tower",
|
|
4
|
-
"description": "Mission planning, isolated worktrees, and review-gated merges
|
|
4
|
+
"description": "Mission planning, isolated worktrees, worker/reviewer agents, and review-gated merges."
|
|
5
|
+
},
|
|
6
|
+
"components": {
|
|
7
|
+
"tower-bundle": {
|
|
8
|
+
"title": "Tower",
|
|
9
|
+
"summary": "Mission planning, isolated worktrees, worker/reviewer agents, and merge gates.",
|
|
10
|
+
"disables": "Turns off the whole Tower bundle; no orphan preset or skill is left behind."
|
|
11
|
+
},
|
|
12
|
+
"tower-skills": {
|
|
13
|
+
"title": "Tower skills",
|
|
14
|
+
"summary": "Playbooks for worker/reviewer agents and recovery after a blocked merge.",
|
|
15
|
+
"disables": "Internal section; toggled with the Tower bundle."
|
|
16
|
+
},
|
|
17
|
+
"preset-tower-worker": {
|
|
18
|
+
"title": "Tower worker",
|
|
19
|
+
"summary": "Writes code, runs verification, and commits only the files assigned to its mission.",
|
|
20
|
+
"disables": "Internal section; a missing preset is rejected before planning/mode entry."
|
|
21
|
+
},
|
|
22
|
+
"preset-tower-reviewer": {
|
|
23
|
+
"title": "Tower reviewer",
|
|
24
|
+
"summary": "Read-only checks for scope and acceptance, returning a clean verdict or findings.",
|
|
25
|
+
"disables": "Internal section; a missing preset is rejected before planning/mode entry when review is required."
|
|
26
|
+
}
|
|
5
27
|
}
|
|
6
28
|
}
|
package/locale/zh.json
CHANGED
|
@@ -2,5 +2,27 @@
|
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Tower",
|
|
4
4
|
"description": "多代理任务规划、隔离工作树与审查门禁合并。"
|
|
5
|
+
},
|
|
6
|
+
"components": {
|
|
7
|
+
"tower-bundle": {
|
|
8
|
+
"title": "Tower 控制塔",
|
|
9
|
+
"summary": "任务规划、隔离工作树、执行/审查代理与合并门禁。",
|
|
10
|
+
"disables": "关闭后整个 Tower 一起停用;不会留下孤立的代理预设或技能。"
|
|
11
|
+
},
|
|
12
|
+
"tower-skills": {
|
|
13
|
+
"title": "Tower 技能包",
|
|
14
|
+
"summary": "执行与审查代理的操作手册,以及失败后的恢复指南。",
|
|
15
|
+
"disables": "内部 section;随 Tower 整体启停。"
|
|
16
|
+
},
|
|
17
|
+
"preset-tower-worker": {
|
|
18
|
+
"title": "Tower 执行代理",
|
|
19
|
+
"summary": "在独立分支里写代码、跑验证并提交,只改分给它的文件。",
|
|
20
|
+
"disables": "内部 section;缺失时会在规划/进入模式前被拒绝。"
|
|
21
|
+
},
|
|
22
|
+
"preset-tower-reviewer": {
|
|
23
|
+
"title": "Tower 审查代理",
|
|
24
|
+
"summary": "只读检查改动是否越界、是否达标,给出通过或问题清单。",
|
|
25
|
+
"disables": "内部 section;缺失且强制审查时会在规划/进入模式前被拒绝。"
|
|
26
|
+
}
|
|
5
27
|
}
|
|
6
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tower",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.1.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH-native Tower: mission planning, worktree isolation, review-gated merges, and a human-owned Tower mode.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"url": "git+https://github.com/louifinn/dsh-tower.git"
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/louifinn/dsh-tower#readme",
|
|
22
|
+
"types": "types/index.d.ts",
|
|
22
23
|
"bugs": {
|
|
23
24
|
"url": "https://github.com/louifinn/dsh-tower/issues"
|
|
24
25
|
},
|
|
@@ -41,17 +42,25 @@
|
|
|
41
42
|
"skills.js",
|
|
42
43
|
"skills/",
|
|
43
44
|
"lib/*.js",
|
|
45
|
+
"domain/**/*.js",
|
|
46
|
+
"app/*.js",
|
|
47
|
+
"tools/*.js",
|
|
48
|
+
"adapters/*.js",
|
|
49
|
+
"types/",
|
|
44
50
|
"cordis.patch.yml",
|
|
45
51
|
"scripts/",
|
|
46
52
|
"locale/*.json",
|
|
47
53
|
"icon.svg",
|
|
48
54
|
"README.md",
|
|
49
55
|
"README.en.md",
|
|
56
|
+
"COMPATIBILITY.md",
|
|
50
57
|
"CHANGELOG.md",
|
|
51
58
|
"LICENSE",
|
|
52
59
|
"CONTRIBUTING.md",
|
|
53
60
|
"SECURITY.md",
|
|
54
|
-
"docs/"
|
|
61
|
+
"docs/",
|
|
62
|
+
"ui-bundle/",
|
|
63
|
+
"ports/*.js"
|
|
55
64
|
],
|
|
56
65
|
"dsh": {
|
|
57
66
|
"bundle": {
|
|
@@ -62,11 +71,31 @@
|
|
|
62
71
|
"test": "node --test tests/*.test.mjs",
|
|
63
72
|
"test:watch": "node --test --watch tests/*.test.mjs",
|
|
64
73
|
"test:coverage": "node --test --experimental-test-coverage tests/*.test.mjs",
|
|
74
|
+
"smoke:mount": "node scripts/mount-smoke.mjs",
|
|
75
|
+
"smoke:fixtures": "node scripts/mount-smoke/fixtures.mjs",
|
|
76
|
+
"release:check": "node scripts/release-check.mjs",
|
|
77
|
+
"audit:issues": "node scripts/issue-audit.mjs",
|
|
65
78
|
"dev:hmr": "node scripts/enable-dev-hmr.mjs",
|
|
66
79
|
"docs:check": "node scripts/check-docs.mjs",
|
|
67
|
-
"
|
|
80
|
+
"docs:generate:errors": "node scripts/generate-error-reference.mjs",
|
|
81
|
+
"docs:check:generated": "node scripts/generate-error-reference.mjs --check",
|
|
82
|
+
"docs:generate:tools": "node scripts/generate-tools-reference.mjs",
|
|
83
|
+
"docs:check:generated:tools": "node scripts/generate-tools-reference.mjs --check",
|
|
84
|
+
"docs:generate:config": "node scripts/generate-config-reference.mjs",
|
|
85
|
+
"docs:check:generated:config": "node scripts/generate-config-reference.mjs --check",
|
|
86
|
+
"pack:check": "node scripts/check-pack.mjs",
|
|
87
|
+
"architecture:check": "node scripts/architecture.mjs",
|
|
88
|
+
"types:check": "tsc --noEmit",
|
|
89
|
+
"check": "npm test && npm run docs:check && npm run pack:check && npm run architecture:check && npm run types:check && npm run domain:check",
|
|
90
|
+
"domain:metrics": "node scripts/domain-metrics.mjs",
|
|
91
|
+
"domain:check": "node scripts/domain-metrics.mjs --check"
|
|
68
92
|
},
|
|
69
93
|
"dependencies": {
|
|
70
|
-
"@deepseek-ai/schemastery": "~3.18.5"
|
|
94
|
+
"@deepseek-ai/schemastery": "~3.18.5",
|
|
95
|
+
"@sinclair/typebox": "^0.34.41"
|
|
96
|
+
},
|
|
97
|
+
"devDependencies": {
|
|
98
|
+
"@types/node": "^22.20.5",
|
|
99
|
+
"typescript": "~5.9.3"
|
|
71
100
|
}
|
|
72
101
|
}
|
package/ports/clock.js
ADDED
package/ports/ids.js
ADDED
package/ports/index.js
ADDED
package/ports/store.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Store port (#56). Implementation: adapters/store-fs.js.
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} StorePort
|
|
4
|
+
* @property {(file: string) => Promise<boolean>} exists
|
|
5
|
+
* @property {(dir: string) => Promise<void>} ensureDir
|
|
6
|
+
* @property {(file: string) => Promise<any>} readJson
|
|
7
|
+
* @property {(file: string, value: unknown) => Promise<void>} writeJsonAtomic
|
|
8
|
+
* @property {(file: string, value: unknown) => Promise<void>} appendJsonl
|
|
9
|
+
* @property {(paths: object, fn: () => Promise<any>, options?: object) => Promise<any>} withLock
|
|
10
|
+
*/
|
|
11
|
+
export {}
|
package/ports/vcs.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vcs port (#56). Implementation: adapters/vcs-git.js.
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} VcsPort
|
|
4
|
+
* @property {(cwd: string, args: string[], options?: object) => Promise<string>} git
|
|
5
|
+
* @property {(cwd: string, args: string[], options?: object) => Promise<string|null>} tryGit
|
|
6
|
+
* @property {(cwd: string, ref: string) => Promise<string>} revParse
|
|
7
|
+
* @property {(cwd: string) => Promise<boolean>} isClean
|
|
8
|
+
* @property {(cwd: string, a: string, b: string) => Promise<boolean>} isAncestor
|
|
9
|
+
* @property {(repoRoot: string) => Promise<{root:string, commonDir:string, branch:string|null}>} resolveRepo
|
|
10
|
+
*/
|
|
11
|
+
export {}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Architecture fitness functions (#55).
|
|
2
|
+
//
|
|
3
|
+
// Rules are intentionally staged: rules for not-yet-migrated areas are
|
|
4
|
+
// warnings (printed, exit 0) and flip to errors in the PR that makes them true.
|
|
5
|
+
// The evaluator is exported so tests can seed one fixture violation per rule.
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const ROOTS = ['lib', 'app', 'domain', 'tools', 'adapters', 'ports']
|
|
12
|
+
const CHILD_PROCESS_ALLOWED = new Set(['adapters/vcs-git.js'])
|
|
13
|
+
const FS_ALLOWED = new Set(['adapters/store-fs.js'])
|
|
14
|
+
// Composition roots own the only intentional process-wide mutable state.
|
|
15
|
+
const AMBIENT_ALLOWED = new Set(['lib/config.js', 'lib/git.js'])
|
|
16
|
+
// The registration aggregator wires tool modules together; the thinness rule
|
|
17
|
+
// applies to the individual modules under tools/.
|
|
18
|
+
const TOOLS_REGISTRATION = new Set(['tools/index.js'])
|
|
19
|
+
// The registration aggregator is the only tools/ module above the 200-line
|
|
20
|
+
// per-tool ceiling: it wires the individual modules together. P3c split
|
|
21
|
+
// tower_spawn, so every tool module now enforces the ceiling.
|
|
22
|
+
const TOOLS_SIZE_ALLOWED = new Set(['tools/index.js'])
|
|
23
|
+
const LITERAL_PATTERN = /enum:\s*\[\s*'(?:planned|active|completed|blocked|paused|abandoned|worker|reviewer|bug|improve|vuln|idea|low|medium|high|critical|open|triaged|fixed|wontfix)'/
|
|
24
|
+
const TOOL_LITERAL_PATTERN = /(?:===|!==|==|!=|\.includes\(|case\s+)'?(?:planned|active|completed|blocked|paused|abandoned|worker|reviewer|bug|improve|vuln|idea|low|medium|high|critical|open|triaged|fixed|wontfix|create|abandon|close|status|merge|fix-then-merge|hold|acquire|release|map|user|failure|resource|dependency|json|markdown)'/
|
|
25
|
+
|
|
26
|
+
function walk(root, relative = '') {
|
|
27
|
+
const dir = path.join(root, relative)
|
|
28
|
+
if (!fs.existsSync(dir)) return []
|
|
29
|
+
const out = []
|
|
30
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
31
|
+
const next = path.join(relative, entry.name)
|
|
32
|
+
if (entry.isDirectory()) out.push(...walk(root, next))
|
|
33
|
+
else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) out.push(next.split(path.sep).join('/'))
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function evaluator(root) {
|
|
39
|
+
const files = ROOTS.flatMap((dir) => walk(root, dir)).filter((file) => !file.includes('/node_modules/'))
|
|
40
|
+
const violations = []
|
|
41
|
+
const record = (rule, file, detail, level) => violations.push({ rule, file, detail, level })
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const text = fs.readFileSync(path.join(root, file), 'utf8')
|
|
45
|
+
const lines = text.split('\n')
|
|
46
|
+
const imports = [...text.matchAll(/from\s+'([^']+)'|require\('([^']+)'\)/g)].map((match) => match[1] ?? match[2])
|
|
47
|
+
if (file.startsWith('domain/') && imports.some((spec) => /^(node:fs|node:fs\/promises|node:child_process|\.\.\/adapters|adapters\/)/.test(spec))) {
|
|
48
|
+
record('domain-pure', file, 'domain imports I/O or adapters', 'error')
|
|
49
|
+
}
|
|
50
|
+
if (imports.includes('node:child_process') && !CHILD_PROCESS_ALLOWED.has(file)) {
|
|
51
|
+
record('child-process-surface', file, 'only adapters/** may spawn processes', 'error')
|
|
52
|
+
}
|
|
53
|
+
if (imports.some((spec) => spec === 'node:fs' || spec === 'node:fs/promises') && !FS_ALLOWED.has(file)) {
|
|
54
|
+
record('fs-surface', file, 'only adapters/** may use fs', 'error')
|
|
55
|
+
}
|
|
56
|
+
if (file.startsWith('tools/')) {
|
|
57
|
+
if (imports.some((spec) => spec.startsWith('node:') && spec !== 'node:path')) {
|
|
58
|
+
record('tools-thin', file, 'tools/** may import only node:path', 'error')
|
|
59
|
+
}
|
|
60
|
+
if (LITERAL_PATTERN.test(text) || TOOL_LITERAL_PATTERN.test(text)) {
|
|
61
|
+
record('tools-vocabulary', file, 'status/type/role literal in tools/**', 'error')
|
|
62
|
+
}
|
|
63
|
+
if (lines.length < 40 || lines.length > 200) {
|
|
64
|
+
record('aggregate-size', file, `${lines.length} lines outside 40-200`, 'error')
|
|
65
|
+
}
|
|
66
|
+
} else if (lines.length > 800) {
|
|
67
|
+
record('aggregate-size', file, `${lines.length} lines exceeds 800`, 'error')
|
|
68
|
+
}
|
|
69
|
+
if (/^(?:export\s+)?let\s+/m.test(text) && !AMBIENT_ALLOWED.has(file) && (file.startsWith('lib/') || file.startsWith('app/') || file.startsWith('domain/'))) {
|
|
70
|
+
record('ambient-state', file, 'top-level let outside an allow-list', 'error')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: violations.every((violation) => violation.level !== 'error'), violations }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rootArg = process.argv.indexOf('--root')
|
|
77
|
+
const root = rootArg > 0 ? path.resolve(process.argv[rootArg + 1]) : PACKAGE_ROOT
|
|
78
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
79
|
+
if (isMain) {
|
|
80
|
+
const result = evaluator(root)
|
|
81
|
+
if (process.argv.includes('--json')) {
|
|
82
|
+
console.log(JSON.stringify(result, null, 2))
|
|
83
|
+
} else {
|
|
84
|
+
for (const violation of result.violations) {
|
|
85
|
+
const line = `architecture ${violation.level}: [${violation.rule}] ${violation.file}: ${violation.detail}`
|
|
86
|
+
if (violation.level === 'error') console.error(line)
|
|
87
|
+
else console.warn(line)
|
|
88
|
+
}
|
|
89
|
+
if (result.violations.length === 0) console.log('architecture: ok')
|
|
90
|
+
}
|
|
91
|
+
process.exit(result.ok ? 0 : 1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { evaluator }
|
package/scripts/check-docs.mjs
CHANGED
|
@@ -9,14 +9,20 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
9
9
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
|
|
10
10
|
const exists = (file) => fs.existsSync(path.join(root, file))
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const toolModuleSources = fs.existsSync(path.join(root, 'tools'))
|
|
13
|
+
? fs.readdirSync(path.join(root, 'tools'))
|
|
14
|
+
.filter((name) => name.endsWith('.js'))
|
|
15
|
+
.map((name) => read(`tools/${name}`))
|
|
16
|
+
.join('\n')
|
|
17
|
+
: ''
|
|
18
|
+
const toolsSource = read('tools/index.js') + toolModuleSources + read('lib/tool-registry.js') + read('index.js')
|
|
13
19
|
const documentedTools = read('docs/reference/tools.md')
|
|
14
20
|
const toolNames = [...new Set([...toolsSource.matchAll(/name: '(tower_[a-z_]+)'/g)].map((match) => match[1]))]
|
|
15
21
|
.filter((name) => name !== 'tower-control')
|
|
16
22
|
.sort()
|
|
17
23
|
const missingTools = toolNames.filter((name) => !documentedTools.includes(`\`${name}\``))
|
|
18
24
|
|
|
19
|
-
const towerSource = read('
|
|
25
|
+
const towerSource = read('app/tower.js')
|
|
20
26
|
const blockersSource = read('docs/reference/blockers.md')
|
|
21
27
|
const blockerCodes = new Set()
|
|
22
28
|
for (const match of towerSource.matchAll(/push\(\s*'([a-z][a-z-]+)'/g)) blockerCodes.add(match[1])
|
|
@@ -77,16 +83,20 @@ const eventNames = [
|
|
|
77
83
|
'status',
|
|
78
84
|
'preflight',
|
|
79
85
|
'doctor',
|
|
86
|
+
'recover',
|
|
80
87
|
'test',
|
|
81
88
|
'batch',
|
|
82
89
|
'resource',
|
|
83
90
|
'import_legacy',
|
|
84
91
|
'legacy_map',
|
|
92
|
+
'mode',
|
|
93
|
+
'control',
|
|
94
|
+
'reload',
|
|
85
95
|
]
|
|
86
96
|
const documentedEvents = read('docs/reference/events.md')
|
|
87
97
|
const missingEvents = eventNames.filter((name) => !documentedEvents.includes(`\`tower/${name}\``))
|
|
88
98
|
|
|
89
|
-
const errorsSource = read('
|
|
99
|
+
const errorsSource = read('domain/errors.js')
|
|
90
100
|
const documentedErrors = read('docs/reference/errors.md')
|
|
91
101
|
const errorCodes = [...new Set([...errorsSource.matchAll(/^ {2}'([a-z][a-z-]+)':/gm)].map((match) => match[1]))].sort()
|
|
92
102
|
const missingErrors = errorCodes.filter((code) => !documentedErrors.includes(`\`${code}\``))
|
|
@@ -102,6 +112,37 @@ if (missingErrors.length > 0) problems.push(`docs/reference/errors.md misses cod
|
|
|
102
112
|
if (missingEvents.length > 0) problems.push(`docs/reference/events.md misses events: ${missingEvents.join(', ')}`)
|
|
103
113
|
if (missingChangelog) problems.push(`CHANGELOG.md misses version ${packageJson.version}`)
|
|
104
114
|
|
|
115
|
+
// Counts and high-risk defaults are part of the contract, not just names.
|
|
116
|
+
const toolsDoc = read('docs/reference/tools.md')
|
|
117
|
+
const blockersDoc = read('docs/reference/blockers.md')
|
|
118
|
+
const errorsDoc = read('docs/reference/errors.md')
|
|
119
|
+
const eventsDoc = read('docs/reference/events.md')
|
|
120
|
+
const configDoc = read('docs/reference/config.md')
|
|
121
|
+
const zhConfigDoc = read('docs/zh/reference/config.md')
|
|
122
|
+
if (!toolsDoc.includes(`up to ${toolNames.length} tools`)) {
|
|
123
|
+
problems.push(`docs/reference/tools.md must state "up to ${toolNames.length} tools"`)
|
|
124
|
+
}
|
|
125
|
+
if (!blockersDoc.includes(`${blockerCodes.size} stable blocker/failure codes`)) {
|
|
126
|
+
problems.push(`docs/reference/blockers.md must state ${blockerCodes.size} stable blocker/failure codes`)
|
|
127
|
+
}
|
|
128
|
+
if (!errorsDoc.includes(`${errorCodes.length} tool error codes`)) {
|
|
129
|
+
problems.push(`docs/reference/errors.md must state ${errorCodes.length} tool error codes`)
|
|
130
|
+
}
|
|
131
|
+
if (!eventsDoc.includes(`${eventNames.length} process-local events`)) {
|
|
132
|
+
problems.push(`docs/reference/events.md must state ${eventNames.length} process-local events`)
|
|
133
|
+
}
|
|
134
|
+
for (const required of [
|
|
135
|
+
'| `tests.admission` | `off` |',
|
|
136
|
+
'| `review.requireReview` | `false` |',
|
|
137
|
+
'| `mode.allowAgentControl` | `true` |',
|
|
138
|
+
'| `git.timeoutMs` | `15000` |',
|
|
139
|
+
]) {
|
|
140
|
+
if (!configDoc.includes(required)) problems.push(`docs/reference/config.md misses required default: ${required}`)
|
|
141
|
+
}
|
|
142
|
+
if (!zhConfigDoc.includes('| `tests.admission` | `off` |')) {
|
|
143
|
+
problems.push('docs/zh/reference/config.md misses the tests.admission default')
|
|
144
|
+
}
|
|
145
|
+
|
|
105
146
|
// Chinese parity: when the translation tree exists it must cover the same
|
|
106
147
|
// reference names, so a tool or code cannot be added only in English.
|
|
107
148
|
if (exists('docs/zh')) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Fail when the published file list drifts from the reviewed snapshot.
|
|
2
|
+
// Run: node scripts/check-pack.mjs [--write]
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const manifestFile = path.join(root, 'scripts', 'pack-manifest.txt')
|
|
10
|
+
const write = process.argv.includes('--write')
|
|
11
|
+
|
|
12
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
13
|
+
let parsed
|
|
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)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const entry = Array.isArray(parsed) ? parsed[0] : parsed
|
|
28
|
+
const paths = (entry?.files ?? []).map((file) => file.path).filter(Boolean).sort()
|
|
29
|
+
if (write) {
|
|
30
|
+
fs.writeFileSync(manifestFile, `${paths.join('\n')}\n`)
|
|
31
|
+
console.log(`pack check: wrote ${paths.length} paths to ${path.relative(root, manifestFile)}`)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
const expected = fs.readFileSync(manifestFile, 'utf8').split('\n').filter(Boolean).sort()
|
|
35
|
+
const added = paths.filter((file) => !expected.includes(file))
|
|
36
|
+
const removed = expected.filter((file) => !paths.includes(file))
|
|
37
|
+
if (added.length > 0 || removed.length > 0) {
|
|
38
|
+
console.error('pack check: published file list changed; review and update scripts/pack-manifest.txt')
|
|
39
|
+
if (added.length > 0) console.error(` added: ${added.join(', ')}`)
|
|
40
|
+
if (removed.length > 0) console.error(` removed: ${removed.join(', ')}`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
const required = ['cordis.patch.yml', 'index.js', 'locale/en.json', 'locale/zh.json', 'ui-bundle/index.js', 'COMPATIBILITY.md']
|
|
44
|
+
const missing = required.filter((file) => !paths.includes(file))
|
|
45
|
+
if (missing.length > 0) {
|
|
46
|
+
console.error(`pack check: required runtime files missing: ${missing.join(', ')}`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
console.log(`pack check: ${paths.length} files match scripts/pack-manifest.txt`)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
6
|
+
const SOURCE_ROOTS = ['domain', 'app', 'tools', 'adapters', 'lib']
|
|
7
|
+
const ENTRY_FILES = ['index.js', 'worker.js', 'reviewer.js', 'skills.js']
|
|
8
|
+
const PURE_ROOTS = ['domain']
|
|
9
|
+
|
|
10
|
+
function walk(dir, out = []) {
|
|
11
|
+
if (!fs.existsSync(dir)) return out
|
|
12
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
const full = path.join(dir, entry.name)
|
|
14
|
+
if (entry.isDirectory()) walk(full, out)
|
|
15
|
+
else if (entry.name.endsWith('.js') || entry.name.endsWith('.mjs')) out.push(full)
|
|
16
|
+
}
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loc(file) {
|
|
21
|
+
return fs.readFileSync(file, 'utf8').split('\n').length
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = SOURCE_ROOTS.flatMap((dir) => walk(path.join(root, dir)))
|
|
25
|
+
for (const file of ENTRY_FILES) {
|
|
26
|
+
const full = path.join(root, file)
|
|
27
|
+
if (fs.existsSync(full)) files.push(full)
|
|
28
|
+
}
|
|
29
|
+
let pure = 0
|
|
30
|
+
let total = 0
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const lines = loc(file)
|
|
33
|
+
total += lines
|
|
34
|
+
const relative = path.relative(root, file).split(path.sep).join('/')
|
|
35
|
+
if (PURE_ROOTS.some((prefix) => relative === prefix || relative.startsWith(`${prefix}/`))) pure += lines
|
|
36
|
+
}
|
|
37
|
+
const share = total === 0 ? 0 : pure / total
|
|
38
|
+
const result = { pure, total, share: Number(share.toFixed(4)) }
|
|
39
|
+
if (process.argv.includes('--json')) console.log(JSON.stringify(result))
|
|
40
|
+
else if (process.argv.includes('--check')) {
|
|
41
|
+
if (share < 0.35) {
|
|
42
|
+
console.error(`domain-metrics: pure domain share ${(share * 100).toFixed(1)}% < 35% (${pure}/${total})`)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
console.log(`domain-metrics: pure domain share ${(share * 100).toFixed(1)}% (${pure}/${total})`)
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`domain-metrics: pure domain share ${(share * 100).toFixed(1)}% (${pure}/${total})`)
|
|
48
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Generate the config reference from the schemastery schema (#46).
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { Config, resolveTowerConfig } from '../domain/config/schema.js'
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
+
const target = path.join(root, 'docs', 'reference', 'config.generated.md')
|
|
9
|
+
const defaults = resolveTowerConfig({})
|
|
10
|
+
const rows = []
|
|
11
|
+
for (const [key, schema] of Object.entries(Config.dict ?? {})) {
|
|
12
|
+
const dict = schema?.dict
|
|
13
|
+
if (dict && typeof dict === 'object') {
|
|
14
|
+
for (const [child, childSchema] of Object.entries(dict)) {
|
|
15
|
+
rows.push(format(`${key}.${child}`, childSchema, schema))
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
rows.push(format(key, schema, Config))
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function format(key, schema, parent) {
|
|
22
|
+
const value = key.split('.').reduce((current, part) => (current == null ? undefined : current[part]), defaults)
|
|
23
|
+
const fallback = value === undefined ? '' : JSON.stringify(value)
|
|
24
|
+
const description = String(schema?.meta?.description ?? parent?.meta?.description ?? '').replace(/\s+/g, ' ').trim()
|
|
25
|
+
return `| \`${key}\` | \`${fallback}\` | ${description} |`
|
|
26
|
+
}
|
|
27
|
+
const expected = `# Config reference (generated)
|
|
28
|
+
|
|
29
|
+
> Generated by \`npm run docs:generate:config\` from \`domain/config/schema.js\`. Values are defaults before repo config.
|
|
30
|
+
|
|
31
|
+
| Key | Default | Description |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
${rows.join('\n')}
|
|
34
|
+
`
|
|
35
|
+
if (process.argv.includes('--check')) {
|
|
36
|
+
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
37
|
+
if (actual !== expected) {
|
|
38
|
+
console.error('config reference is stale; run npm run docs:generate:config')
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
console.log(`config reference: ${rows.length} keys in sync`)
|
|
42
|
+
} else {
|
|
43
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
44
|
+
fs.writeFileSync(target, expected)
|
|
45
|
+
console.log(`wrote ${path.relative(root, target)} (${rows.length} keys)`)
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generate the error-code reference from domain/errors.js so the catalog and docs
|
|
2
|
+
// cannot disagree (#46). Usage: node scripts/generate-error-reference.mjs [--check]
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { ERROR_CODES } from '../domain/errors.js'
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
const target = path.join(root, 'docs', 'reference', 'errors.generated.md')
|
|
10
|
+
const expected = `# Error code catalog (generated)
|
|
11
|
+
|
|
12
|
+
> Generated by \`npm run docs:generate:errors\` from \`domain/errors.js\`. Do not edit by hand.
|
|
13
|
+
|
|
14
|
+
| Code | Description |
|
|
15
|
+
|---|---|
|
|
16
|
+
${Object.entries(ERROR_CODES).map(([code, description]) => `| \`${code}\` | ${description} |`).join('\n')}
|
|
17
|
+
`
|
|
18
|
+
if (process.argv.includes('--check')) {
|
|
19
|
+
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
20
|
+
if (actual !== expected) {
|
|
21
|
+
console.error(`error reference is stale; run npm run docs:generate:errors`)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
console.log(`error reference: ${Object.keys(ERROR_CODES).length} codes in sync`)
|
|
25
|
+
} else {
|
|
26
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
27
|
+
fs.writeFileSync(target, expected)
|
|
28
|
+
console.log(`wrote ${path.relative(root, target)} (${Object.keys(ERROR_CODES).length} codes)`)
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Generate the tools overview from the registered tool definitions (#46).
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { registerTowerTools } from '../tools/index.js'
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
+
const target = path.join(root, 'docs', 'reference', 'tools.generated.md')
|
|
9
|
+
const tools = new Map()
|
|
10
|
+
const ctx = {
|
|
11
|
+
get() {
|
|
12
|
+
return undefined
|
|
13
|
+
},
|
|
14
|
+
effect(fn) {
|
|
15
|
+
const dispose = fn()
|
|
16
|
+
return typeof dispose === 'function' ? dispose : () => {}
|
|
17
|
+
},
|
|
18
|
+
systemPrompt: { section() {} },
|
|
19
|
+
tools: {
|
|
20
|
+
register(definition) {
|
|
21
|
+
tools.set(definition.name, definition)
|
|
22
|
+
return () => {}
|
|
23
|
+
},
|
|
24
|
+
guard() {
|
|
25
|
+
return () => {}
|
|
26
|
+
},
|
|
27
|
+
restrict() {
|
|
28
|
+
return () => {}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
registerTowerTools(ctx)
|
|
33
|
+
const rows = [...tools.values()]
|
|
34
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
35
|
+
.map((tool) => `| \`${tool.name}\` | ${String(tool.description ?? '').replace(/\s+/g, ' ').trim()} |`)
|
|
36
|
+
const expected = `# Tools overview (generated)
|
|
37
|
+
|
|
38
|
+
> Generated by \`npm run docs:generate:tools\` from the registered tool definitions. Do not edit by hand.
|
|
39
|
+
|
|
40
|
+
| Tool | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
${rows.join('\n')}
|
|
43
|
+
`
|
|
44
|
+
if (process.argv.includes('--check')) {
|
|
45
|
+
const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
46
|
+
if (actual !== expected) {
|
|
47
|
+
console.error('tools reference is stale; run npm run docs:generate:tools')
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
console.log(`tools reference: ${tools.size} tools in sync`)
|
|
51
|
+
} else {
|
|
52
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
53
|
+
fs.writeFileSync(target, expected)
|
|
54
|
+
console.log(`wrote ${path.relative(root, target)} (${tools.size} tools)`)
|
|
55
|
+
}
|
|
56
|
+
process.exit(0)
|