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
package/locale/zh.json
CHANGED
|
@@ -4,25 +4,45 @@
|
|
|
4
4
|
"description": "多代理任务规划、隔离工作树与审查门禁合并。"
|
|
5
5
|
},
|
|
6
6
|
"components": {
|
|
7
|
-
"tower
|
|
7
|
+
"tower": {
|
|
8
8
|
"title": "Tower 控制塔",
|
|
9
|
-
"summary": "
|
|
10
|
-
"disables": "
|
|
9
|
+
"summary": "任务规划、隔离工作树、模式守卫、健康契约与恢复逃生口。",
|
|
10
|
+
"disables": "关闭强制主组件等同于卸载 Tower:不再保留任何工具、preset 或技能。"
|
|
11
11
|
},
|
|
12
12
|
"tower-skills": {
|
|
13
13
|
"title": "Tower 技能包",
|
|
14
14
|
"summary": "执行与审查代理的操作手册,以及失败后的恢复指南。",
|
|
15
|
-
"disables": "
|
|
15
|
+
"disables": "三个 Tower 技能从目录中移除;执行/审查协议段落与所有 mission 工具继续工作。"
|
|
16
16
|
},
|
|
17
|
-
"
|
|
18
|
-
"title": "
|
|
19
|
-
"summary": "
|
|
20
|
-
"disables": "
|
|
17
|
+
"tower-batch": {
|
|
18
|
+
"title": "证据批次",
|
|
19
|
+
"summary": "把 mission 归入有限证据批次,并按必需检查与回执关闭批次。",
|
|
20
|
+
"disables": "不再注册 tower_batch;mission、门禁与合并不受影响。"
|
|
21
|
+
},
|
|
22
|
+
"tower-import": {
|
|
23
|
+
"title": "旧版导入",
|
|
24
|
+
"summary": "导入 Tower Codex 旧状态文件,并审计快照到 mission 的映射。",
|
|
25
|
+
"disables": "不再注册 tower_import_legacy 与 tower_legacy_map;新状态不受影响。"
|
|
26
|
+
},
|
|
27
|
+
"tower-leases": {
|
|
28
|
+
"title": "咨询式租约",
|
|
29
|
+
"summary": "用咨询式资源租约协调每个仓库中的重型工作。",
|
|
30
|
+
"disables": "不再注册 tower_resource;其他能力不依赖租约。"
|
|
31
|
+
},
|
|
32
|
+
"tower-tests": {
|
|
33
|
+
"title": "测试发现",
|
|
34
|
+
"summary": "运行 worktree 声明的测试发现,并为 mission tip 记录回执。",
|
|
35
|
+
"disables": "不再注册 tower_test,也无法记录回执;若 gates.requireTests:true,规划/进入模式会以解释性错误拒绝。"
|
|
36
|
+
},
|
|
37
|
+
"tower-hmr": {
|
|
38
|
+
"title": "开发热重载",
|
|
39
|
+
"summary": "为联动开发 profile 注册 tower_reload 与 /tower reload。",
|
|
40
|
+
"disables": "tower_reload 与 /tower reload 不可用;非 HMR profile 的行为与之前完全相同。随包默认关闭。"
|
|
21
41
|
},
|
|
22
42
|
"preset-tower-reviewer": {
|
|
23
43
|
"title": "Tower 审查代理",
|
|
24
|
-
"summary": "
|
|
25
|
-
"disables": "
|
|
44
|
+
"summary": "只读独立审查:检查 mission diff、核对范围与验收,给出结论。",
|
|
45
|
+
"disables": "无法派发审查代理。review.requireReview 为 false 时允许关闭;强制审查开启时规划/进入模式会被锁定并给出解释。"
|
|
26
46
|
}
|
|
27
47
|
}
|
|
28
48
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tower",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
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",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"./worker": "./worker.js",
|
|
32
32
|
"./reviewer": "./reviewer.js",
|
|
33
33
|
"./skills": "./skills.js",
|
|
34
|
+
"./batch": "./host/capability-batch.js",
|
|
35
|
+
"./import": "./host/capability-import.js",
|
|
36
|
+
"./leases": "./host/capability-leases.js",
|
|
37
|
+
"./tests": "./host/capability-tests.js",
|
|
38
|
+
"./hmr": "./host/capability-hmr.js",
|
|
34
39
|
"./locale/*.json": "./locale/*.json",
|
|
35
40
|
"./icon": "./icon.svg",
|
|
36
41
|
"./package.json": "./package.json"
|
|
@@ -41,7 +46,7 @@
|
|
|
41
46
|
"reviewer.js",
|
|
42
47
|
"skills.js",
|
|
43
48
|
"skills/",
|
|
44
|
-
"
|
|
49
|
+
"host/*.js",
|
|
45
50
|
"domain/**/*.js",
|
|
46
51
|
"app/*.js",
|
|
47
52
|
"tools/*.js",
|
|
@@ -58,6 +63,7 @@
|
|
|
58
63
|
"LICENSE",
|
|
59
64
|
"CONTRIBUTING.md",
|
|
60
65
|
"SECURITY.md",
|
|
66
|
+
"PRINCIPLES.md",
|
|
61
67
|
"docs/",
|
|
62
68
|
"ui-bundle/",
|
|
63
69
|
"ports/*.js"
|
|
@@ -71,22 +77,36 @@
|
|
|
71
77
|
"test": "node --test tests/*.test.mjs",
|
|
72
78
|
"test:watch": "node --test --watch tests/*.test.mjs",
|
|
73
79
|
"test:coverage": "node --test --experimental-test-coverage tests/*.test.mjs",
|
|
80
|
+
"test:live": "node tests/live/run.cjs",
|
|
74
81
|
"smoke:mount": "node scripts/mount-smoke.mjs",
|
|
75
82
|
"smoke:fixtures": "node scripts/mount-smoke/fixtures.mjs",
|
|
76
83
|
"release:check": "node scripts/release-check.mjs",
|
|
77
84
|
"audit:issues": "node scripts/issue-audit.mjs",
|
|
85
|
+
"audit:regressions": "node scripts/regression-audit.mjs",
|
|
86
|
+
"surface:check": "node scripts/surface-baseline.mjs",
|
|
78
87
|
"dev:hmr": "node scripts/enable-dev-hmr.mjs",
|
|
79
88
|
"docs:check": "node scripts/check-docs.mjs",
|
|
89
|
+
"docs:check:philosophy": "node scripts/check-philosophy.mjs",
|
|
80
90
|
"docs:generate:errors": "node scripts/generate-error-reference.mjs",
|
|
81
91
|
"docs:check:generated": "node scripts/generate-error-reference.mjs --check",
|
|
82
92
|
"docs:generate:tools": "node scripts/generate-tools-reference.mjs",
|
|
83
93
|
"docs:check:generated:tools": "node scripts/generate-tools-reference.mjs --check",
|
|
84
94
|
"docs:generate:config": "node scripts/generate-config-reference.mjs",
|
|
85
95
|
"docs:check:generated:config": "node scripts/generate-config-reference.mjs --check",
|
|
96
|
+
"docs:generate:events": "node scripts/generate-events-reference.mjs",
|
|
97
|
+
"docs:check:generated:events": "node scripts/generate-events-reference.mjs --check",
|
|
98
|
+
"types:generate": "node scripts/generate-types.mjs",
|
|
99
|
+
"docs:check:generated:types": "node scripts/generate-types.mjs --check",
|
|
100
|
+
"docs:generate:components": "node scripts/generate-component-surface.mjs",
|
|
101
|
+
"docs:check:generated:components": "node scripts/generate-component-surface.mjs --check",
|
|
102
|
+
"docs:check:components": "node scripts/check-components.mjs",
|
|
86
103
|
"pack:check": "node scripts/check-pack.mjs",
|
|
87
104
|
"architecture:check": "node scripts/architecture.mjs",
|
|
88
|
-
"types:check": "tsc --noEmit",
|
|
89
|
-
"
|
|
105
|
+
"types:check": "tsc --noEmit && tsc -p tsconfig.strict.json",
|
|
106
|
+
"types:coverage": "node scripts/types-coverage.mjs",
|
|
107
|
+
"types:strict:check": "node scripts/types-strict.mjs",
|
|
108
|
+
"types:strict:write": "node scripts/types-strict.mjs --write",
|
|
109
|
+
"check": "npm test && npm run docs:check && npm run docs:check:philosophy && npm run docs:check:generated && npm run docs:check:generated:tools && npm run docs:check:generated:config && npm run docs:check:generated:events && npm run docs:check:generated:types && npm run docs:check:generated:components && npm run docs:check:components && npm run surface:check && npm run pack:check && npm run architecture:check && npm run types:coverage && npm run types:strict:check && npm run types:check && npm run domain:check",
|
|
90
110
|
"domain:metrics": "node scripts/domain-metrics.mjs",
|
|
91
111
|
"domain:check": "node scripts/domain-metrics.mjs --check"
|
|
92
112
|
},
|
package/ports/archive.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Terminal-mission archive port (#69). The readers/writers are store-agnostic:
|
|
2
|
+
// they take the store port and the Tower paths, so the same archive logic runs
|
|
3
|
+
// on the filesystem adapter and on an in-memory store.
|
|
4
|
+
import { assertPort, definePort } from './contract.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} ArchivePort
|
|
8
|
+
* @property {(store: unknown, paths: { dir: string }) => Promise<{ ok: boolean, file: string, missions: any[], error?: string, exists: boolean }>} readIndex
|
|
9
|
+
* @property {(store: unknown, paths: { dir: string }, missionId: string) => Promise<any>} readEntry
|
|
10
|
+
* @property {(store: unknown, paths: { dir: string }, input: { entries: any[], now?: number }) => Promise<any>} writeEntries
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const ARCHIVE_PORT = definePort('ArchivePort', ['readIndex', 'readEntry', 'writeEntries'])
|
|
14
|
+
|
|
15
|
+
/** @param {unknown} impl */
|
|
16
|
+
export function assertArchivePort(impl) {
|
|
17
|
+
return /** @type {ArchivePort} */ (assertPort(impl, ARCHIVE_PORT, 'archive'))
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Case-sensitivity probe port (#69). tower_init probes the checkout filesystem
|
|
2
|
+
// once; a composition without a real filesystem injects its own answer.
|
|
3
|
+
import { assertPort, definePort } from './contract.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} CaseSensitivityPort
|
|
7
|
+
* @property {(root: string) => Promise<boolean | null>} detect
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const CASE_SENSITIVITY_PORT = definePort('CaseSensitivityPort', ['detect'])
|
|
11
|
+
|
|
12
|
+
/** @param {unknown} impl */
|
|
13
|
+
export function assertCaseSensitivityPort(impl) {
|
|
14
|
+
return /** @type {CaseSensitivityPort} */ (assertPort(impl, CASE_SENSITIVITY_PORT, 'caseSensitivity'))
|
|
15
|
+
}
|
package/ports/clock.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
// Clock port (#
|
|
1
|
+
// Clock port (#69).
|
|
2
|
+
import { assertPort, definePort } from './contract.js'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* @typedef {object} ClockPort
|
|
4
6
|
* @property {() => number} now
|
|
5
7
|
*/
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
export const CLOCK_PORT = definePort('ClockPort', ['now'])
|
|
10
|
+
|
|
11
|
+
/** @param {unknown} impl */
|
|
12
|
+
export function assertClockPort(impl) {
|
|
13
|
+
return /** @type {ClockPort} */ (assertPort(impl, CLOCK_PORT, 'clock'))
|
|
14
|
+
}
|
package/ports/config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Config port (#69).
|
|
2
|
+
//
|
|
3
|
+
// App use cases read the effective Tower config through this port. They do not
|
|
4
|
+
// import host/config.js, so the process-level config store stays a property of
|
|
5
|
+
// the composition root rather than a module-global dependency of the app.
|
|
6
|
+
import { assertPort, definePort } from './contract.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} ConfigPort
|
|
10
|
+
* @property {() => import('../types/index.js').TowerConfig} runtimeConfig
|
|
11
|
+
* @property {(repo: { root: string, commonDir: string, branch?: string|null }) => Promise<import('../types/index.js').TowerConfig>} resolveRepoConfig
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const CONFIG_PORT = definePort('ConfigPort', ['runtimeConfig', 'resolveRepoConfig'])
|
|
15
|
+
|
|
16
|
+
/** @param {unknown} impl */
|
|
17
|
+
export function assertConfigPort(impl) {
|
|
18
|
+
return /** @type {ConfigPort} */ (assertPort(impl, CONFIG_PORT, 'config'))
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Port contract primitives (#69).
|
|
2
|
+
//
|
|
3
|
+
// A port is a runtime-checkable contract, not only a JSDoc typedef: the
|
|
4
|
+
// contract object is importable, and `assertPort` rejects a dependency object
|
|
5
|
+
// that does not implement it. That is what makes the seam substitutable at
|
|
6
|
+
// runtime instead of merely documented.
|
|
7
|
+
export function definePort(name, methods) {
|
|
8
|
+
return Object.freeze({ name, methods: Object.freeze([...methods]) })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} impl
|
|
13
|
+
* @param {{ name: string, methods: readonly string[] }} contract
|
|
14
|
+
* @param {string} [label]
|
|
15
|
+
*/
|
|
16
|
+
export function assertPort(impl, contract, label = contract.name) {
|
|
17
|
+
if (!impl || typeof impl !== 'object') {
|
|
18
|
+
throw new TypeError(`${label}: expected an object implementing ${contract.name}`)
|
|
19
|
+
}
|
|
20
|
+
const missing = contract.methods.filter((method) => typeof (/** @type {any} */ (impl))[method] !== 'function')
|
|
21
|
+
if (missing.length > 0) {
|
|
22
|
+
throw new TypeError(`${label} does not implement ${contract.name}; missing: ${missing.join(', ')}`)
|
|
23
|
+
}
|
|
24
|
+
return impl
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {unknown} impl
|
|
29
|
+
* @param {{ name: string, methods: readonly string[] }} contract
|
|
30
|
+
*/
|
|
31
|
+
export function implementsPort(impl, contract) {
|
|
32
|
+
try {
|
|
33
|
+
assertPort(impl, contract)
|
|
34
|
+
return true
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
package/ports/events.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Domain-event port (#75).
|
|
2
|
+
//
|
|
3
|
+
// The app layer persists a transition and then emits the domain events it
|
|
4
|
+
// described through this port. The host binds it to Cordis `ctx.emit`; tests
|
|
5
|
+
// bind it to a recorder. `emit` is best effort by contract: a listener (or a
|
|
6
|
+
// missing service) must never fail the tool that produced the fact.
|
|
7
|
+
import { assertPort, definePort } from './contract.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} EventPort
|
|
11
|
+
* @property {(name: string, payload: Record<string, any>) => void} emit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const EVENT_PORT = definePort('EventPort', ['emit'])
|
|
15
|
+
|
|
16
|
+
/** @param {unknown} impl */
|
|
17
|
+
export function assertEventPort(impl) {
|
|
18
|
+
return /** @type {EventPort} */ (assertPort(impl, EVENT_PORT, 'events'))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The no-op default: embedders without an event channel still get a Tower. */
|
|
22
|
+
export const NOOP_EVENT_PORT = Object.freeze({
|
|
23
|
+
emit() {},
|
|
24
|
+
})
|
package/ports/ids.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
// Ids port (#
|
|
1
|
+
// Ids port (#69).
|
|
2
|
+
import { assertPort, definePort } from './contract.js'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* @typedef {object} IdsPort
|
|
4
6
|
* @property {() => string} uuid
|
|
5
7
|
* @property {(text: string) => string} shortHash
|
|
6
8
|
*/
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
export const IDS_PORT = definePort('IdsPort', ['uuid', 'shortHash'])
|
|
11
|
+
|
|
12
|
+
/** @param {unknown} impl */
|
|
13
|
+
export function assertIdsPort(impl) {
|
|
14
|
+
return /** @type {IdsPort} */ (assertPort(impl, IDS_PORT, 'ids'))
|
|
15
|
+
}
|
package/ports/index.js
CHANGED
|
@@ -1,2 +1,54 @@
|
|
|
1
|
-
// Explicit ports consumed by app use cases (#
|
|
2
|
-
|
|
1
|
+
// Explicit ports consumed by app use cases (#69).
|
|
2
|
+
//
|
|
3
|
+
// `assertPorts` is the injection check the app layer runs on the dependency
|
|
4
|
+
// object it receives: a composition root that forgets a port fails at
|
|
5
|
+
// construction, not in the middle of a mission.
|
|
6
|
+
import { assertPort } from './contract.js'
|
|
7
|
+
import { STORE_PORT, assertStorePort } from './store.js'
|
|
8
|
+
import { VCS_PORT, assertVcsPort } from './vcs.js'
|
|
9
|
+
import { CLOCK_PORT, assertClockPort } from './clock.js'
|
|
10
|
+
import { IDS_PORT, assertIdsPort } from './ids.js'
|
|
11
|
+
import { CONFIG_PORT, assertConfigPort } from './config.js'
|
|
12
|
+
import { STATE_PORT, assertStatePort } from './state.js'
|
|
13
|
+
import { OWNERSHIP_PORT, assertOwnershipPort } from './ownership.js'
|
|
14
|
+
import { CASE_SENSITIVITY_PORT, assertCaseSensitivityPort } from './caseSensitivity.js'
|
|
15
|
+
import { ARCHIVE_PORT, assertArchivePort } from './archive.js'
|
|
16
|
+
import { EVENT_PORT, assertEventPort } from './events.js'
|
|
17
|
+
|
|
18
|
+
/** Every port the Tower app layer depends on, keyed by deps member. */
|
|
19
|
+
export const PORTS = Object.freeze({
|
|
20
|
+
store: { contract: STORE_PORT, assert: assertStorePort },
|
|
21
|
+
vcs: { contract: VCS_PORT, assert: assertVcsPort },
|
|
22
|
+
clock: { contract: CLOCK_PORT, assert: assertClockPort },
|
|
23
|
+
ids: { contract: IDS_PORT, assert: assertIdsPort },
|
|
24
|
+
config: { contract: CONFIG_PORT, assert: assertConfigPort },
|
|
25
|
+
state: { contract: STATE_PORT, assert: assertStatePort },
|
|
26
|
+
ownership: { contract: OWNERSHIP_PORT, assert: assertOwnershipPort },
|
|
27
|
+
caseSensitivity: { contract: CASE_SENSITIVITY_PORT, assert: assertCaseSensitivityPort },
|
|
28
|
+
archive: { contract: ARCHIVE_PORT, assert: assertArchivePort },
|
|
29
|
+
events: { contract: EVENT_PORT, assert: assertEventPort },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** @param {unknown} deps */
|
|
33
|
+
export function assertPorts(deps) {
|
|
34
|
+
if (!deps || typeof deps !== 'object') {
|
|
35
|
+
throw new TypeError('Tower requires an injected deps object assembled by the composition root (see adapters/default.js)')
|
|
36
|
+
}
|
|
37
|
+
for (const [name, port] of Object.entries(PORTS)) {
|
|
38
|
+
if (typeof port.assert === 'function') port.assert(deps[name])
|
|
39
|
+
else assertPort(deps[name], port.contract, name)
|
|
40
|
+
}
|
|
41
|
+
return deps
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { STORE_PORT, assertStorePort } from './store.js'
|
|
45
|
+
export { VCS_PORT, assertVcsPort } from './vcs.js'
|
|
46
|
+
export { CLOCK_PORT, assertClockPort } from './clock.js'
|
|
47
|
+
export { IDS_PORT, assertIdsPort } from './ids.js'
|
|
48
|
+
export { CONFIG_PORT, assertConfigPort } from './config.js'
|
|
49
|
+
export { STATE_PORT, assertStatePort } from './state.js'
|
|
50
|
+
export { OWNERSHIP_PORT, assertOwnershipPort } from './ownership.js'
|
|
51
|
+
export { CASE_SENSITIVITY_PORT, assertCaseSensitivityPort } from './caseSensitivity.js'
|
|
52
|
+
export { ARCHIVE_PORT, assertArchivePort } from './archive.js'
|
|
53
|
+
export { EVENT_PORT, assertEventPort, NOOP_EVENT_PORT } from './events.js'
|
|
54
|
+
export { definePort, assertPort, implementsPort } from './contract.js'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Worktree-ownership port (#69). The default implementation verifies a stored
|
|
2
|
+
// worktree against the real filesystem and Git; an in-memory composition can
|
|
3
|
+
// substitute a verifier that matches its own virtual paths.
|
|
4
|
+
import { assertPort, definePort } from './contract.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} OwnershipPort
|
|
8
|
+
* @property {(input: { storedPath: string, storedRealPath?: string, branch: string, commonDir: string, missionId?: string }) => Promise<{ path: string, realPath: string }>} assertOwnership
|
|
9
|
+
* @property {(target: string) => Promise<string>} canonicalPath
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const OWNERSHIP_PORT = definePort('OwnershipPort', ['assertOwnership', 'canonicalPath'])
|
|
13
|
+
|
|
14
|
+
/** @param {unknown} impl */
|
|
15
|
+
export function assertOwnershipPort(impl) {
|
|
16
|
+
return /** @type {OwnershipPort} */ (assertPort(impl, OWNERSHIP_PORT, 'ownership'))
|
|
17
|
+
}
|
package/ports/state.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// State-load port (#69): schema inspection, migration, and the one persisted
|
|
2
|
+
// migration result. The implementation is store-agnostic and receives the
|
|
3
|
+
// store port, so an in-memory store can drive the same load path.
|
|
4
|
+
import { assertPort, definePort } from './contract.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} StatePort
|
|
8
|
+
* @property {(input: { store: unknown, paths: unknown, now?: number, persist?: boolean, migrations?: readonly unknown[] }) => Promise<any>} loadStateFile
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const STATE_PORT = definePort('StatePort', ['loadStateFile'])
|
|
12
|
+
|
|
13
|
+
/** @param {unknown} impl */
|
|
14
|
+
export function assertStatePort(impl) {
|
|
15
|
+
return /** @type {StatePort} */ (assertPort(impl, STATE_PORT, 'state'))
|
|
16
|
+
}
|
package/ports/store.js
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
|
-
// Store port (#
|
|
1
|
+
// Store port (#69). Implementation: adapters/store-fs.js.
|
|
2
|
+
//
|
|
3
|
+
// App use cases depend on this contract, not on adapters/store.js: they receive the
|
|
4
|
+
// implementation through the composition root and never import the adapter.
|
|
5
|
+
// The contract lists every method app/** and tools/** call, so an implementation
|
|
6
|
+
// that is missing one fails `assertPorts` at construction instead of mid-mission.
|
|
7
|
+
import { assertPort, definePort } from './contract.js'
|
|
8
|
+
|
|
2
9
|
/**
|
|
3
10
|
* @typedef {object} StorePort
|
|
11
|
+
* @property {(commonDir: string) => { dir: string, state: string, lock: string, gitLock: string, activity: string }} pathsFor
|
|
4
12
|
* @property {(file: string) => Promise<boolean>} exists
|
|
5
13
|
* @property {(dir: string) => Promise<void>} ensureDir
|
|
6
14
|
* @property {(file: string) => Promise<any>} readJson
|
|
7
15
|
* @property {(file: string, value: unknown) => Promise<void>} writeJsonAtomic
|
|
8
16
|
* @property {(file: string, value: unknown) => Promise<void>} appendJsonl
|
|
9
|
-
* @property {(paths: object, fn: () => Promise<any>, options?: object) => Promise<any>} withLock
|
|
17
|
+
* @property {(paths: object, fn: (paths: object) => Promise<any>, options?: object) => Promise<any>} withLock
|
|
18
|
+
* @property {(paths: object, fn: (paths: object) => Promise<any>, options?: object) => Promise<any>} withGitLock
|
|
19
|
+
* @property {(file: string, limit?: number) => Promise<any[]>} readActivityTail
|
|
20
|
+
* @property {(file: string) => Promise<string>} readText
|
|
21
|
+
* @property {(file: string, text: string, encoding?: string) => Promise<void>} writeText
|
|
22
|
+
* @property {(file: string, text: string, encoding?: string) => Promise<void>} appendText
|
|
23
|
+
* @property {(from: string, to: string) => Promise<void>} rename
|
|
24
|
+
* @property {(from: string, to: string) => Promise<void>} renameWithRetry
|
|
25
|
+
* @property {(file: string) => Promise<{ mtimeMs: number }>} stat
|
|
26
|
+
* @property {(file: string) => Promise<void>} unlink
|
|
27
|
+
* @property {(paths: object, options?: object) => Promise<{ record: any, raw: string | null }>} readGitLock
|
|
28
|
+
* @property {(lock: { record: any } | null, options?: object) => { held: boolean, pid?: number | null, phase?: string | null, heldMs?: number }} gitLockView
|
|
10
29
|
*/
|
|
11
|
-
|
|
30
|
+
|
|
31
|
+
// `rename` and `renameWithRetry` are both declared because they are not
|
|
32
|
+
// interchangeable: `renameWithRetry` has temp-file semantics and on a terminal
|
|
33
|
+
// failure it unlinks its source, which is right for a disposable temp file but
|
|
34
|
+
// would delete the bytes `tower_recover` must keep auditable. The audit rename
|
|
35
|
+
// therefore uses the plain `rename` primitive (see tools/recover.js).
|
|
36
|
+
export const STORE_PORT = definePort('StorePort', [
|
|
37
|
+
'pathsFor',
|
|
38
|
+
'exists',
|
|
39
|
+
'ensureDir',
|
|
40
|
+
'readJson',
|
|
41
|
+
'writeJsonAtomic',
|
|
42
|
+
'appendJsonl',
|
|
43
|
+
'withLock',
|
|
44
|
+
'withGitLock',
|
|
45
|
+
'readActivityTail',
|
|
46
|
+
'readText',
|
|
47
|
+
'writeText',
|
|
48
|
+
'appendText',
|
|
49
|
+
'rename',
|
|
50
|
+
'renameWithRetry',
|
|
51
|
+
'stat',
|
|
52
|
+
'unlink',
|
|
53
|
+
'readGitLock',
|
|
54
|
+
'gitLockView',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
/** @param {unknown} impl */
|
|
58
|
+
export function assertStorePort(impl) {
|
|
59
|
+
return /** @type {StorePort} */ (assertPort(impl, STORE_PORT, 'store'))
|
|
60
|
+
}
|
package/ports/vcs.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
// Vcs port (#
|
|
1
|
+
// Vcs port (#69). Implementation: adapters/vcs-git.js.
|
|
2
|
+
import { assertPort, definePort } from './contract.js'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* @typedef {object} VcsPort
|
|
4
6
|
* @property {(cwd: string, args: string[], options?: object) => Promise<string>} git
|
|
5
7
|
* @property {(cwd: string, args: string[], options?: object) => Promise<string|null>} tryGit
|
|
6
8
|
* @property {(cwd: string, ref: string) => Promise<string>} revParse
|
|
7
9
|
* @property {(cwd: string) => Promise<boolean>} isClean
|
|
8
|
-
* @property {(cwd: string,
|
|
9
|
-
* @property {(repoRoot: string) => Promise<{root:string, commonDir:string, branch:string|null}>} resolveRepo
|
|
10
|
+
* @property {(cwd: string, ancestor: string, descendant: string) => Promise<boolean>} isAncestor
|
|
11
|
+
* @property {(repoRoot: string) => Promise<{ root: string, commonDir: string, branch: string|null }>} resolveRepo
|
|
12
|
+
* @property {(cwd: string) => Promise<string|null>} currentBranch
|
|
13
|
+
* @property {(cwd: string, fromRef: string) => Promise<number>} countCommits
|
|
14
|
+
* @property {(cwd: string) => Promise<Array<{ path: string, branch?: string, head?: string }>>} listWorktrees
|
|
10
15
|
*/
|
|
11
|
-
|
|
16
|
+
|
|
17
|
+
export const VCS_PORT = definePort('VcsPort', [
|
|
18
|
+
'git',
|
|
19
|
+
'tryGit',
|
|
20
|
+
'revParse',
|
|
21
|
+
'isClean',
|
|
22
|
+
'isAncestor',
|
|
23
|
+
'resolveRepo',
|
|
24
|
+
'currentBranch',
|
|
25
|
+
'countCommits',
|
|
26
|
+
'listWorktrees',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** @param {unknown} impl */
|
|
30
|
+
export function assertVcsPort(impl) {
|
|
31
|
+
return /** @type {VcsPort} */ (assertPort(impl, VCS_PORT, 'vcs'))
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Justified architecture-gate exemptions (#80). Every entry needs rule, path, reason, and owner or ref. Scripts/architecture.mjs fails with waiver-contract when an entry is not justified. domain-pure, host-scope and no-lib-from-core cannot be waived at all, and domain/** accepts no waiver. Since #70 the retired lib/ layer has no waivers: host/** owns ambient state by rule, and only the two I/O adapters below are exempt.",
|
|
3
|
+
"waivers": [
|
|
4
|
+
{
|
|
5
|
+
"rule": "child-process-surface",
|
|
6
|
+
"path": "adapters/vcs-git.js",
|
|
7
|
+
"reason": "The git adapter is the single process-spawning boundary; no other module implements VcsPort with a child process.",
|
|
8
|
+
"owner": "louifinn",
|
|
9
|
+
"ref": "#50"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"rule": "fs-surface",
|
|
13
|
+
"path": "adapters/store-fs.js",
|
|
14
|
+
"reason": "The filesystem adapter is the single node:fs boundary; the store port needs atomic writes, lock files, and raw store reads.",
|
|
15
|
+
"owner": "louifinn",
|
|
16
|
+
"ref": "#50"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|