dsh-tower 2.0.4 → 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 +101 -0
- package/COMPATIBILITY.md +38 -0
- package/CONTRIBUTING.md +150 -2
- package/README.en.md +40 -3
- package/README.md +38 -3
- package/SECURITY.md +27 -0
- 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 +3 -1
- package/docs/architecture/overview.md +111 -12
- package/docs/board.md +72 -0
- package/docs/guide/hot-reload.md +1 -1
- package/docs/guide/install-upgrade-migration.md +8 -1
- package/docs/guide/release.md +125 -0
- package/docs/operations/security.md +47 -2
- package/docs/reference/config.generated.md +66 -0
- package/docs/reference/config.md +4 -1
- package/docs/reference/errors.generated.md +39 -0
- package/docs/reference/errors.md +4 -2
- package/docs/reference/tools.generated.md +33 -0
- package/docs/reference/tools.md +1 -1
- package/docs/zh/README.md +3 -2
- package/docs/zh/guide/hot-reload.md +1 -1
- package/docs/zh/guide/install-upgrade-migration.md +6 -1
- package/docs/zh/operations/security.md +30 -1
- package/docs/zh/reference/config.md +4 -1
- package/docs/zh/reference/errors.md +3 -1
- package/docs/zh/reference/tools.md +1 -1
- 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 +63 -20
- package/lib/briefing.js +176 -0
- package/lib/compatibility.js +108 -0
- package/lib/config.js +33 -375
- package/lib/doctor.js +97 -4
- package/lib/git.js +60 -16
- 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 +11 -43
- 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 +54 -137
- package/lib/tests.js +11 -10
- package/lib/tool-registry.js +544 -0
- package/lib/workspaceLock.js +2 -2
- package/lib/worktreeOwnership.js +5 -4
- package/locale/en.json +23 -1
- package/locale/zh.json +22 -0
- package/package.json +32 -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 +9 -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/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/lib/errors.js +0 -123
- package/lib/tools.js +0 -2460
- package/lib/tower.js +0 -2912
- /package/{lib → domain}/batch.js +0 -0
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,18 +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
61
|
"docs/",
|
|
55
|
-
"ui-bundle/"
|
|
62
|
+
"ui-bundle/",
|
|
63
|
+
"ports/*.js"
|
|
56
64
|
],
|
|
57
65
|
"dsh": {
|
|
58
66
|
"bundle": {
|
|
@@ -63,11 +71,31 @@
|
|
|
63
71
|
"test": "node --test tests/*.test.mjs",
|
|
64
72
|
"test:watch": "node --test --watch tests/*.test.mjs",
|
|
65
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",
|
|
66
78
|
"dev:hmr": "node scripts/enable-dev-hmr.mjs",
|
|
67
79
|
"docs:check": "node scripts/check-docs.mjs",
|
|
68
|
-
"
|
|
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"
|
|
69
92
|
},
|
|
70
93
|
"dependencies": {
|
|
71
|
-
"@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"
|
|
72
100
|
}
|
|
73
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])
|
|
@@ -90,7 +96,7 @@ const eventNames = [
|
|
|
90
96
|
const documentedEvents = read('docs/reference/events.md')
|
|
91
97
|
const missingEvents = eventNames.filter((name) => !documentedEvents.includes(`\`tower/${name}\``))
|
|
92
98
|
|
|
93
|
-
const errorsSource = read('
|
|
99
|
+
const errorsSource = read('domain/errors.js')
|
|
94
100
|
const documentedErrors = read('docs/reference/errors.md')
|
|
95
101
|
const errorCodes = [...new Set([...errorsSource.matchAll(/^ {2}'([a-z][a-z-]+)':/gm)].map((match) => match[1]))].sort()
|
|
96
102
|
const missingErrors = errorCodes.filter((code) => !documentedErrors.includes(`\`${code}\``))
|
|
@@ -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)
|