dsh-tower 2.1.0 → 2.3.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/COMPATIBILITY.md +5 -1
  3. package/CONTRIBUTING.md +64 -5
  4. package/PRINCIPLES.md +145 -0
  5. package/README.en.md +7 -4
  6. package/README.md +7 -4
  7. package/SECURITY.md +6 -0
  8. package/{lib → adapters}/archive.js +1 -1
  9. package/{lib → adapters}/caseSensitivity.js +1 -1
  10. package/adapters/default.js +51 -10
  11. package/{lib → adapters}/git.js +1 -1
  12. package/{lib → adapters}/leases.js +1 -1
  13. package/{lib → adapters}/legacy.js +1 -1
  14. package/adapters/persistence.js +543 -0
  15. package/adapters/store-fs.js +28 -1
  16. package/adapters/store.js +204 -0
  17. package/{lib → adapters}/tests.js +9 -5
  18. package/adapters/vcs-git.js +15 -1
  19. package/{lib → adapters}/workspaceLock.js +1 -1
  20. package/{lib → adapters}/worktreeOwnership.js +1 -1
  21. package/app/archive.js +4 -6
  22. package/app/batch.js +9 -10
  23. package/app/board.js +2 -4
  24. package/app/checkCompletion.js +9 -10
  25. package/app/enrichedMissions.js +2 -4
  26. package/app/findings.js +6 -7
  27. package/app/gate.js +1 -2
  28. package/app/init.js +24 -29
  29. package/app/merge.js +58 -51
  30. package/app/messages.js +7 -8
  31. package/app/mission.js +5 -5
  32. package/app/plan.js +8 -5
  33. package/app/provision.js +12 -14
  34. package/app/read.js +6 -3
  35. package/app/rebase.js +14 -16
  36. package/app/recording.js +7 -8
  37. package/app/review.js +15 -8
  38. package/app/roster.js +14 -15
  39. package/{lib → app}/spawn-prechecks.js +4 -3
  40. package/app/spawn.js +12 -13
  41. package/app/teardown.js +8 -9
  42. package/app/tower.js +89 -35
  43. package/docs/README.md +6 -2
  44. package/docs/adr/0001-ports-adapters-as-data.md +49 -0
  45. package/docs/adr/0002-single-state-json.md +50 -0
  46. package/docs/adr/0003-allow-agent-control-default-true.md +48 -0
  47. package/docs/adr/0004-surface-budget.md +50 -0
  48. package/docs/adr/README.md +51 -0
  49. package/docs/agents/workflow.md +51 -0
  50. package/docs/architecture/overview.md +167 -36
  51. package/docs/guide/hot-reload.md +1 -1
  52. package/docs/guide/release.md +1 -1
  53. package/docs/operations/incident.md +82 -0
  54. package/docs/operations/runbook.md +12 -3
  55. package/docs/reference/blockers.md +2 -2
  56. package/docs/reference/config.generated.md +1 -0
  57. package/docs/reference/doctor.md +48 -2
  58. package/docs/reference/errors.generated.md +41 -37
  59. package/docs/reference/errors.md +53 -41
  60. package/docs/reference/events.md +38 -6
  61. package/docs/reference/surface-budget.md +128 -0
  62. package/docs/reference/tools.generated.md +5 -4
  63. package/docs/reference/tools.md +4 -4
  64. package/docs/zh/guide/hot-reload.md +1 -1
  65. package/docs/zh/reference/blockers.md +2 -2
  66. package/docs/zh/reference/doctor.md +29 -1
  67. package/docs/zh/reference/tools.md +4 -4
  68. package/domain/batch.js +2 -2
  69. package/domain/config/schema.js +25 -1
  70. package/domain/contracts/outputs.js +6 -2
  71. package/domain/contracts/tool-inputs/ops.js +1 -0
  72. package/domain/errors.js +297 -42
  73. package/domain/events.js +207 -0
  74. package/domain/integration/gate.js +17 -2
  75. package/domain/integration/merge.js +35 -0
  76. package/domain/lockBudget.js +30 -0
  77. package/domain/missions/completion.js +14 -0
  78. package/domain/missions/lifecycle.js +9 -3
  79. package/domain/missions/plan.js +14 -0
  80. package/domain/missions/review.js +90 -7
  81. package/domain/read/views.js +3 -0
  82. package/domain/state/archive.js +1 -1
  83. package/domain/state.js +1 -1
  84. package/domain/surface.js +248 -0
  85. package/{lib → host}/doctor.js +116 -9
  86. package/{lib → host}/modeStore.js +1 -1
  87. package/{lib → host}/tool-registry.js +106 -31
  88. package/host/tower.js +32 -0
  89. package/index.js +12 -9
  90. package/package.json +10 -3
  91. package/ports/archive.js +18 -0
  92. package/ports/caseSensitivity.js +15 -0
  93. package/ports/clock.js +10 -2
  94. package/ports/config.js +19 -0
  95. package/ports/contract.js +38 -0
  96. package/ports/events.js +24 -0
  97. package/ports/ids.js +10 -2
  98. package/ports/index.js +54 -2
  99. package/ports/ownership.js +17 -0
  100. package/ports/state.js +16 -0
  101. package/ports/store.js +52 -3
  102. package/ports/vcs.js +25 -4
  103. package/scripts/architecture-waivers.json +19 -0
  104. package/scripts/architecture.mjs +634 -23
  105. package/scripts/check-docs.mjs +8 -35
  106. package/scripts/check-pack.mjs +68 -39
  107. package/scripts/check-philosophy.mjs +266 -0
  108. package/scripts/domain-metrics.mjs +13 -4
  109. package/scripts/generate-config-reference.mjs +14 -1
  110. package/scripts/generate-error-reference.mjs +68 -14
  111. package/scripts/generate-events-reference.mjs +83 -0
  112. package/scripts/generate-tools-reference.mjs +11 -0
  113. package/scripts/issue-audit.mjs +2 -2
  114. package/scripts/lib/surface.mjs +87 -0
  115. package/scripts/pack-manifest.txt +58 -22
  116. package/scripts/regression-audit.mjs +164 -0
  117. package/scripts/release-check.mjs +1 -1
  118. package/scripts/surface-baseline.json +21 -0
  119. package/scripts/surface-baseline.mjs +120 -0
  120. package/tools/adopt.js +2 -3
  121. package/tools/batch.js +2 -3
  122. package/tools/complete.js +2 -3
  123. package/tools/diff.js +3 -3
  124. package/tools/doctor.js +7 -10
  125. package/tools/finding.js +2 -3
  126. package/tools/findings.js +3 -3
  127. package/tools/import-legacy.js +3 -5
  128. package/tools/inbox.js +3 -3
  129. package/tools/index.js +7 -7
  130. package/tools/init.js +3 -6
  131. package/tools/interrupt.js +2 -3
  132. package/tools/legacy-map.js +3 -5
  133. package/tools/merge.js +3 -3
  134. package/tools/mission.js +2 -3
  135. package/tools/plan.js +6 -5
  136. package/tools/preflight.js +2 -3
  137. package/tools/rebase.js +3 -3
  138. package/tools/recover.js +38 -13
  139. package/tools/reload.js +3 -6
  140. package/tools/resource.js +3 -5
  141. package/tools/review.js +3 -4
  142. package/tools/roster.js +2 -3
  143. package/tools/send.js +2 -2
  144. package/tools/spawn.js +8 -5
  145. package/tools/status.js +3 -3
  146. package/tools/teardown.js +2 -3
  147. package/tools/test.js +2 -2
  148. package/types/index.d.ts +1 -1
  149. package/ui-bundle/README.md +37 -3
  150. package/ui-bundle/client.cards.js +356 -0
  151. package/ui-bundle/client.denied.js +142 -0
  152. package/ui-bundle/client.dock.js +333 -0
  153. package/ui-bundle/client.helpers.js +271 -0
  154. package/ui-bundle/client.js +79 -1747
  155. package/ui-bundle/client.locales.js +247 -0
  156. package/ui-bundle/client.mode.js +206 -0
  157. package/ui-bundle/client.overlay.js +64 -0
  158. package/ui-bundle/client.styles.js +151 -0
  159. package/ui-bundle/client.toolview.js +82 -0
  160. package/ui-bundle/package.json +1 -0
  161. package/lib/persistence.js +0 -356
  162. package/lib/store.js +0 -133
  163. /package/{lib → adapters}/stateFile.js +0 -0
  164. /package/{lib → app}/briefing.js +0 -0
  165. /package/{lib → app}/spawn-support.js +0 -0
  166. /package/{lib → host}/compatibility.js +0 -0
  167. /package/{lib → host}/config.js +0 -0
  168. /package/{lib → host}/hmr.js +0 -0
  169. /package/{lib → host}/modeState.js +0 -0
  170. /package/{lib → host}/presets.js +0 -0
@@ -0,0 +1,83 @@
1
+ // Generate docs/reference/events.md from the event table (#75) so event names
2
+ // and payloads cannot drift from the runtime. Usage:
3
+ // node scripts/generate-events-reference.mjs [--check]
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import {
8
+ EVENT_COUNT,
9
+ LEGACY_TOOL_EVENT_TABLE,
10
+ MISSION_EVENT_TABLE,
11
+ } from '../domain/events.js'
12
+
13
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
14
+ const target = path.join(root, 'docs', 'reference', 'events.md')
15
+
16
+ const payloadCell = (entry) => `\`{ ${entry.payload.join(', ')} }\``
17
+ const missionRows = MISSION_EVENT_TABLE.map(
18
+ (entry) => `| \`${entry.name}\` | ${payloadCell(entry)} | ${entry.description} |`,
19
+ ).join('\n')
20
+ const legacyRows = LEGACY_TOOL_EVENT_TABLE.map((entry) => `| \`${entry.name}\` | ${entry.description} |`).join('\n')
21
+
22
+ const expected = `# Tower events
23
+
24
+ > Generated by \`npm run docs:generate:events\` from \`domain/events.js\`. Do not edit by hand.
25
+
26
+ Tower emits process-local Cordis events after successful tool calls (mutations and read-only tools), so
27
+ other plugins can observe a repository without polling. This release defines ${EVENT_COUNT} process-local events:
28
+ ${MISSION_EVENT_TABLE.length} stable domain events and ${LEGACY_TOOL_EVENT_TABLE.length} deprecated tool-shaped
29
+ aliases. Events are **not** sent over the network and carry no telemetry.
30
+
31
+ ## Domain events (stable)
32
+
33
+ Domain events describe what changed, not which tool ran. They are emitted only after the state write
34
+ succeeds, so \`revision\` is the revision a reader can load back from \`state.json\`; a failed tool emits
35
+ no domain event. \`at\` is the transition timestamp in epoch milliseconds. \`state.json\` stays
36
+ authoritative: these events are an observation channel, not an event-sourced log, and are never
37
+ replayed.
38
+
39
+ | Event | Payload | Emitted after |
40
+ |---|---|---|
41
+ ${missionRows}
42
+
43
+ \`\`\`js
44
+ ctx.on('tower.mission.merged', ({ missionId, revision, at, commit }) => {
45
+ // the merge is already durable; commit is the merge commit that landed
46
+ })
47
+ \`\`\`
48
+
49
+ ## Tool-shaped aliases (deprecated)
50
+
51
+ The older \`tower/<tool>\` events are kept for one minor release as a **deprecated** alias for the
52
+ tool-shaped observation channel; prefer the domain events above and expect the alias to be removed in
53
+ the next minor. They carry \`{ tool, repoRoot, value }\`, fire after a successful tool call, and never
54
+ fail a tool when a listener throws. For \`tower_merge\` the alias also fires for a gate failure whose
55
+ \`value.ok\` is false.
56
+
57
+ | Event | Emitted after |
58
+ |---|---|
59
+ ${legacyRows}
60
+
61
+ \`\`\`js
62
+ ctx.on('tower/merge', (payload) => {
63
+ // deprecated: prefer tower.mission.merged
64
+ // payload.tool, payload.repoRoot, payload.value (the tool's JSON result)
65
+ })
66
+ \`\`\`
67
+
68
+ For a durable audit trail use \`<git-common-dir>/dsh-tower/activity.jsonl\` and \`MISSIONS.md\` instead;
69
+ events are process-local and are not replayed.
70
+ `
71
+
72
+ if (process.argv.includes('--check')) {
73
+ const actual = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
74
+ if (actual !== expected) {
75
+ console.error('events reference is stale; run npm run docs:generate:events')
76
+ process.exit(1)
77
+ }
78
+ console.log(`events reference: ${EVENT_COUNT} events in sync`)
79
+ } else {
80
+ fs.mkdirSync(path.dirname(target), { recursive: true })
81
+ fs.writeFileSync(target, expected)
82
+ console.log(`wrote ${path.relative(root, target)} (${EVENT_COUNT} events)`)
83
+ }
@@ -3,9 +3,12 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { registerTowerTools } from '../tools/index.js'
6
+ import { SURFACE_BASELINE_FILE, surfaceLine } from '../domain/surface.js'
7
+ import { readBaseline } from './lib/surface.mjs'
6
8
 
7
9
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
8
10
  const target = path.join(root, 'docs', 'reference', 'tools.generated.md')
11
+ const baseline = readBaseline(root)
9
12
  const tools = new Map()
10
13
  const ctx = {
11
14
  get() {
@@ -33,9 +36,17 @@ registerTowerTools(ctx)
33
36
  const rows = [...tools.values()]
34
37
  .sort((a, b) => a.name.localeCompare(b.name))
35
38
  .map((tool) => `| \`${tool.name}\` | ${String(tool.description ?? '').replace(/\s+/g, ' ').trim()} |`)
39
+ // Live tools count, baseline config counts: `npm run surface:check` keeps the
40
+ // two halves equal, so the generated header carries one decision record.
41
+ const counts = {
42
+ configKeys: baseline.surfaces.configKeys.value,
43
+ sections: baseline.surfaces.sections.value,
44
+ tools: tools.size,
45
+ }
36
46
  const expected = `# Tools overview (generated)
37
47
 
38
48
  > Generated by \`npm run docs:generate:tools\` from the registered tool definitions. Do not edit by hand.
49
+ > Surface: ${surfaceLine(counts)} · baseline ${baseline.date}. Generated from \`${SURFACE_BASELINE_FILE}\`; do not edit by hand.
39
50
 
40
51
  | Tool | Description |
41
52
  |---|---|
@@ -87,7 +87,7 @@ export function selectEvidence(issue) {
87
87
  return null
88
88
  }
89
89
 
90
- function labelsOf(issue) {
90
+ export function labelsOf(issue) {
91
91
  const labels = Array.isArray(issue.labels) ? issue.labels : []
92
92
  return labels.map((label) => (typeof label === 'string' ? label : label?.name ?? '')).filter(Boolean)
93
93
  }
@@ -116,7 +116,7 @@ function closedAtOf(issue) {
116
116
 
117
117
  // A pointer must stay inside the repository and name a real test file: an
118
118
  // existing script or doc is not evidence of a regression test.
119
- function inspectPointer(pointer, root) {
119
+ export function inspectPointer(pointer, root) {
120
120
  const normalized = String(pointer).replace(/\\/g, '/').trim()
121
121
  if (!normalized) return { code: 'invalid-pointer', detail: 'empty' }
122
122
  if (path.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('/')) {
@@ -0,0 +1,87 @@
1
+ // File-reading wrapper around the pure concept-budget surface (#84).
2
+ //
3
+ // `domain/surface.js` owns the rules; this module owns the two files they read
4
+ // (`scripts/surface-baseline.json` and `docs/reference/surface-budget.md`) and
5
+ // the combined check used by the CLI gate and the tests.
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import {
9
+ SURFACE_BASELINE_FILE,
10
+ SURFACE_CLASSIFICATION_FILE,
11
+ parseBaseline,
12
+ validateBaseline,
13
+ validateClassification,
14
+ } from '../../domain/surface.js'
15
+
16
+ /**
17
+ * @param {string} root repository root
18
+ * @returns {ReturnType<typeof parseBaseline>}
19
+ */
20
+ export function readBaseline(root) {
21
+ const file = path.join(root, SURFACE_BASELINE_FILE)
22
+ return parseBaseline(fs.readFileSync(file, 'utf8'))
23
+ }
24
+
25
+ /**
26
+ * @param {string} root repository root
27
+ */
28
+ export function readClassification(root) {
29
+ return fs.readFileSync(path.join(root, SURFACE_CLASSIFICATION_FILE), 'utf8')
30
+ }
31
+
32
+ /**
33
+ * Run both halves of the concept-budget gate against one repository root.
34
+ * `current` carries the live counts and the live key/tool names.
35
+ *
36
+ * @param {{ root: string, current: { configKeys: number, sections: number, tools: number, liveNames: { configKeys: string[], sections: string[], tools: string[] } } }} options
37
+ */
38
+ export function checkSurfaceBudget({ root, current }) {
39
+ let baseline
40
+ try {
41
+ baseline = readBaseline(root)
42
+ } catch (error) {
43
+ return {
44
+ ok: false,
45
+ baseline: null,
46
+ classification: null,
47
+ findings: [
48
+ {
49
+ code: 'baseline-unreadable',
50
+ level: 'error',
51
+ message: `${SURFACE_BASELINE_FILE} is unreadable: ${String(error?.message ?? error)}`,
52
+ },
53
+ ],
54
+ }
55
+ }
56
+ const findings = validateBaseline(baseline, current, {
57
+ exists: (relative) => fs.existsSync(path.join(root, relative)),
58
+ })
59
+ let classificationText
60
+ try {
61
+ classificationText = readClassification(root)
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ baseline,
66
+ classification: null,
67
+ findings: [
68
+ ...findings,
69
+ {
70
+ code: 'classification-unreadable',
71
+ level: 'error',
72
+ message: `${SURFACE_CLASSIFICATION_FILE} is unreadable: ${String(error?.message ?? error)}`,
73
+ },
74
+ ],
75
+ }
76
+ }
77
+ const classification = validateClassification(classificationText, current.liveNames, {
78
+ baselineDate: baseline.date,
79
+ })
80
+ const all = [...findings, ...classification.findings]
81
+ return {
82
+ ok: all.length === 0,
83
+ baseline,
84
+ classification,
85
+ findings: all,
86
+ }
87
+ }
@@ -2,15 +2,28 @@ CHANGELOG.md
2
2
  COMPATIBILITY.md
3
3
  CONTRIBUTING.md
4
4
  LICENSE
5
+ PRINCIPLES.md
5
6
  README.en.md
6
7
  README.md
7
8
  SECURITY.md
9
+ adapters/archive.js
10
+ adapters/caseSensitivity.js
8
11
  adapters/default.js
12
+ adapters/git.js
13
+ adapters/leases.js
14
+ adapters/legacy.js
15
+ adapters/persistence.js
16
+ adapters/stateFile.js
9
17
  adapters/store-fs.js
18
+ adapters/store.js
19
+ adapters/tests.js
10
20
  adapters/vcs-git.js
21
+ adapters/workspaceLock.js
22
+ adapters/worktreeOwnership.js
11
23
  app/archive.js
12
24
  app/batch.js
13
25
  app/board.js
26
+ app/briefing.js
14
27
  app/checkCompletion.js
15
28
  app/enrichedMissions.js
16
29
  app/findings.js
@@ -27,14 +40,22 @@ app/rebase.js
27
40
  app/recording.js
28
41
  app/review.js
29
42
  app/roster.js
43
+ app/spawn-prechecks.js
44
+ app/spawn-support.js
30
45
  app/spawn.js
31
46
  app/teardown.js
32
47
  app/tower.js
33
48
  cordis.patch.yml
34
49
  docs/README.md
50
+ docs/adr/0001-ports-adapters-as-data.md
51
+ docs/adr/0002-single-state-json.md
52
+ docs/adr/0003-allow-agent-control-default-true.md
53
+ docs/adr/0004-surface-budget.md
54
+ docs/adr/README.md
35
55
  docs/agents/domain.md
36
56
  docs/agents/issue-tracker.md
37
57
  docs/agents/triage-labels.md
58
+ docs/agents/workflow.md
38
59
  docs/architecture/overview.md
39
60
  docs/board.md
40
61
  docs/examples/first-mission.md
@@ -44,6 +65,7 @@ docs/guide/install-upgrade-migration.md
44
65
  docs/guide/quickstart.md
45
66
  docs/guide/release.md
46
67
  docs/guide/scenarios-and-philosophy.md
68
+ docs/operations/incident.md
47
69
  docs/operations/runbook.md
48
70
  docs/operations/security.md
49
71
  docs/reference/blockers.md
@@ -54,6 +76,7 @@ docs/reference/errors.generated.md
54
76
  docs/reference/errors.md
55
77
  docs/reference/events.md
56
78
  docs/reference/operations.md
79
+ docs/reference/surface-budget.md
57
80
  docs/reference/testing.md
58
81
  docs/reference/tools.generated.md
59
82
  docs/reference/tools.md
@@ -87,6 +110,7 @@ domain/contracts/tool-inputs/missions.js
87
110
  domain/contracts/tool-inputs/ops.js
88
111
  domain/contracts/tool-inputs/shared.js
89
112
  domain/errors.js
113
+ domain/events.js
90
114
  domain/gate.js
91
115
  domain/integration/gate.js
92
116
  domain/integration/init.js
@@ -96,6 +120,7 @@ domain/integration/provision.js
96
120
  domain/integration/rebase.js
97
121
  domain/integration/review.js
98
122
  domain/integration/teardown.js
123
+ domain/lockBudget.js
99
124
  domain/mission.js
100
125
  domain/missions/completion.js
101
126
  domain/missions/findings.js
@@ -115,50 +140,49 @@ domain/state/migrations.js
115
140
  domain/state/schema.js
116
141
  domain/state/semantics.js
117
142
  domain/summary.js
143
+ domain/surface.js
118
144
  domain/vocabulary.js
145
+ host/compatibility.js
146
+ host/config.js
147
+ host/doctor.js
148
+ host/hmr.js
149
+ host/modeState.js
150
+ host/modeStore.js
151
+ host/presets.js
152
+ host/tool-registry.js
153
+ host/tower.js
119
154
  icon.svg
120
155
  index.js
121
- lib/archive.js
122
- lib/briefing.js
123
- lib/caseSensitivity.js
124
- lib/compatibility.js
125
- lib/config.js
126
- lib/doctor.js
127
- lib/git.js
128
- lib/hmr.js
129
- lib/leases.js
130
- lib/legacy.js
131
- lib/modeState.js
132
- lib/modeStore.js
133
- lib/persistence.js
134
- lib/presets.js
135
- lib/spawn-prechecks.js
136
- lib/spawn-support.js
137
- lib/stateFile.js
138
- lib/store.js
139
- lib/tests.js
140
- lib/tool-registry.js
141
- lib/workspaceLock.js
142
- lib/worktreeOwnership.js
143
156
  locale/en.json
144
157
  locale/zh.json
145
158
  package.json
159
+ ports/archive.js
160
+ ports/caseSensitivity.js
146
161
  ports/clock.js
162
+ ports/config.js
163
+ ports/contract.js
164
+ ports/events.js
147
165
  ports/ids.js
148
166
  ports/index.js
167
+ ports/ownership.js
168
+ ports/state.js
149
169
  ports/store.js
150
170
  ports/vcs.js
151
171
  reviewer.js
172
+ scripts/architecture-waivers.json
152
173
  scripts/architecture.mjs
153
174
  scripts/check-docs.mjs
154
175
  scripts/check-pack.mjs
176
+ scripts/check-philosophy.mjs
155
177
  scripts/domain-metrics.mjs
156
178
  scripts/enable-dev-hmr.mjs
157
179
  scripts/generate-config-reference.mjs
158
180
  scripts/generate-error-reference.mjs
181
+ scripts/generate-events-reference.mjs
159
182
  scripts/generate-tools-reference.mjs
160
183
  scripts/issue-audit.mjs
161
184
  scripts/lib/cli.mjs
185
+ scripts/lib/surface.mjs
162
186
  scripts/lib/tmp.mjs
163
187
  scripts/mount-smoke.mjs
164
188
  scripts/mount-smoke/fixtures.mjs
@@ -166,7 +190,10 @@ scripts/mount-smoke/host.json
166
190
  scripts/mount-smoke/probe.mjs
167
191
  scripts/mount-smoke/surface.mjs
168
192
  scripts/pack-manifest.txt
193
+ scripts/regression-audit.mjs
169
194
  scripts/release-check.mjs
195
+ scripts/surface-baseline.json
196
+ scripts/surface-baseline.mjs
170
197
  skills.js
171
198
  skills/tower-recovery.md
172
199
  skills/tower-review.md
@@ -202,7 +229,16 @@ tools/test.js
202
229
  types/cordis.d.ts
203
230
  types/index.d.ts
204
231
  ui-bundle/README.md
232
+ ui-bundle/client.cards.js
233
+ ui-bundle/client.denied.js
234
+ ui-bundle/client.dock.js
235
+ ui-bundle/client.helpers.js
205
236
  ui-bundle/client.js
237
+ ui-bundle/client.locales.js
238
+ ui-bundle/client.mode.js
239
+ ui-bundle/client.overlay.js
240
+ ui-bundle/client.styles.js
241
+ ui-bundle/client.toolview.js
206
242
  ui-bundle/cordis.patch.yml
207
243
  ui-bundle/icon.svg
208
244
  ui-bundle/index.js
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ // Regression closure audit (#85): a closed `regression` issue must name a
3
+ // `Regression test:` path that exists.
4
+ //
5
+ // A regression is "worked in a released version, then stopped". The project's
6
+ // own closure standard is a pointer, not prose: the incident template
7
+ // (docs/operations/incident.md) carries six fields and the regression test path
8
+ // is the one that can be checked. This script reads a local tracker export, the
9
+ // same format as `npm run audit:issues`, so it never touches the network:
10
+ //
11
+ // { "issues": [ { number, title, state, labels, closedAt, body, comments } ] }
12
+ //
13
+ // Usage:
14
+ // node scripts/regression-audit.mjs [--tracker <file>] [--root <dir>] [--json]
15
+ //
16
+ // Exit codes: 0 all checked regressions point at an existing test; 1 at least
17
+ // one is missing or points at a nonexistent/non-test path; 2 the export is
18
+ // missing or malformed.
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { inspectPointer, isClosed, labelsOf, parseTracker } from './issue-audit.mjs'
23
+ import { findingRecorder, parseArgs, runMain } from './lib/cli.mjs'
24
+
25
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
26
+ const DEFAULT_TRACKER = path.join('tracker', 'issues.json')
27
+ const USAGE = `usage: node scripts/regression-audit.mjs [--tracker <file>] [--root <dir>] [--json]
28
+
29
+ --tracker local tracker export (default: tracker/issues.json)
30
+ --root repository the Regression test paths resolve against (default: this package)
31
+ --json print the report as JSON
32
+ --help print this text
33
+
34
+ A regression issue is closed when state is closed (or closedAt is set) and its
35
+ labels include "regression". Its body or newest comment must carry:
36
+
37
+ Regression test: tests/<file>.test.mjs
38
+
39
+ The path must exist under --root and must be a test file. Produce the export once
40
+ with gh (see CONTRIBUTING.md, "Regressions and incidents"); the audit itself is
41
+ offline.`
42
+
43
+ function cleanValue(value) {
44
+ return String(value)
45
+ .trim()
46
+ .replace(/^`+|`+$/g, '')
47
+ .trim()
48
+ .replace(/^["']+|["']+$/g, '')
49
+ .trim()
50
+ }
51
+
52
+ /**
53
+ * The newest text (body first, then comments oldest to newest) that carries a
54
+ * `Regression test:` line is the closure evidence. Returns `{ pointer }` or
55
+ * `{ error: 'missing' }`.
56
+ * @param {any} issue
57
+ */
58
+ export function selectRegressionField(issue) {
59
+ const sources = []
60
+ if (typeof issue.body === 'string' && issue.body.trim()) sources.push(issue.body)
61
+ const comments = Array.isArray(issue.comments) ? issue.comments : []
62
+ for (const comment of comments) {
63
+ const body = typeof comment === 'string' ? comment : comment?.body
64
+ if (typeof body === 'string' && body.trim()) sources.push(body)
65
+ }
66
+ for (let index = sources.length - 1; index >= 0; index -= 1) {
67
+ for (const rawLine of sources[index].split(/\r?\n/)) {
68
+ const line = rawLine.replace(/^\s*(?:[-*+]\s+|>\s*)+/, '')
69
+ const match = /^Regression test\s*:\s*(.+)$/i.exec(line)
70
+ if (!match) continue
71
+ const pointer = cleanValue(match[1])
72
+ if (pointer) return { pointer }
73
+ }
74
+ }
75
+ return { error: 'missing' }
76
+ }
77
+
78
+ /**
79
+ * Audit closed regression issues against the repository test tree.
80
+ * @param {any[]} issues parsed tracker issues
81
+ * @param {{ root?: string }} [options]
82
+ */
83
+ export function auditRegressions(issues, options = {}) {
84
+ const root = path.resolve(options.root ?? PACKAGE_ROOT)
85
+ const findings = []
86
+ let checked = 0
87
+ for (const issue of issues) {
88
+ if (!isClosed(issue)) continue
89
+ const labels = labelsOf(issue).map((label) => label.toLowerCase())
90
+ if (!labels.includes('regression')) continue
91
+ checked += 1
92
+ const label = `#${issue.number} ${String(issue.title ?? '').trim()}`
93
+ const record = findingRecorder(findings, { number: issue.number, title: issue.title })
94
+ const field = selectRegressionField(issue)
95
+ if (field.error) {
96
+ record(
97
+ 'missing-regression-test',
98
+ 'error',
99
+ `${label}: closed regression has no "Regression test:" field (see docs/operations/incident.md)`,
100
+ )
101
+ continue
102
+ }
103
+ const problem = inspectPointer(field.pointer, root)
104
+ if (!problem) continue
105
+ if (problem.code === 'missing-test') {
106
+ record('missing-regression-test', 'error', `${label}: Regression test names a missing file: ${field.pointer}`)
107
+ } else if (problem.code === 'not-a-test') {
108
+ record('not-a-test', 'error', `${label}: Regression test is not a test file: ${field.pointer} (${problem.detail})`)
109
+ } else {
110
+ record(
111
+ 'invalid-regression-test',
112
+ 'error',
113
+ `${label}: Regression test must be a repository-relative path: ${field.pointer} (${problem.detail})`,
114
+ )
115
+ }
116
+ }
117
+ const errors = findings.filter((item) => item.level === 'error')
118
+ return { ok: errors.length === 0, checked, findings, errors: errors.length }
119
+ }
120
+
121
+ function main(argv) {
122
+ const options = parseArgs(argv, {
123
+ initial: { tracker: null, root: PACKAGE_ROOT, json: false, help: false },
124
+ values: {
125
+ '--tracker': 'tracker',
126
+ '--root': { key: 'root', parse: path.resolve },
127
+ },
128
+ flags: { '--json': 'json', '--help': 'help', '-h': 'help' },
129
+ })
130
+ if (options.help) {
131
+ console.log(USAGE)
132
+ return 0
133
+ }
134
+ const tracker = options.tracker ? path.resolve(process.cwd(), options.tracker) : path.join(PACKAGE_ROOT, DEFAULT_TRACKER)
135
+ let text
136
+ try {
137
+ text = fs.readFileSync(tracker, 'utf8')
138
+ } catch (error) {
139
+ console.error(`regression-audit: cannot read tracker export ${tracker}: ${error?.message ?? error}`)
140
+ console.error('regression-audit: produce one with the gh command in CONTRIBUTING.md ("Regressions and incidents")')
141
+ return 2
142
+ }
143
+ let issues
144
+ let result
145
+ try {
146
+ issues = parseTracker(text)
147
+ result = auditRegressions(issues, { root: options.root })
148
+ } catch (error) {
149
+ console.error(`regression-audit: ${error.message}`)
150
+ return 2
151
+ }
152
+ if (options.json) {
153
+ console.log(JSON.stringify({ tracker, root: path.resolve(options.root), ...result }, null, 2))
154
+ return result.ok ? 0 : 1
155
+ }
156
+ console.log(`regression-audit: ${tracker}`)
157
+ console.log(`regression-audit: ${result.checked} closed regression(s) checked, ${result.errors} error(s)`)
158
+ for (const item of result.findings) console.log(`${item.level} [${item.code}] ${item.message}`)
159
+ if (result.ok) console.log('regression-audit: every closed regression names an existing test file')
160
+ else console.log('regression-audit: a closed regression has no verifiable test pointer; see docs/operations/incident.md')
161
+ return result.ok ? 0 : 1
162
+ }
163
+
164
+ await runMain(import.meta.url, () => main(process.argv.slice(2)), { errorPrefix: 'regression-audit', usage: USAGE })
@@ -27,7 +27,7 @@
27
27
  import fs from 'node:fs'
28
28
  import path from 'node:path'
29
29
  import { spawnSync } from 'node:child_process'
30
- import { compareVersions } from '../lib/compatibility.js'
30
+ import { compareVersions } from '../host/compatibility.js'
31
31
  import { findingRecorder, parseArgs, runMain, UsageError } from './lib/cli.mjs'
32
32
 
33
33
  const USAGE = `usage: node scripts/release-check.mjs [--mode pr|tag] [--tag <name>] [--json] [--help]
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": 1,
3
+ "date": "2026-10-23",
4
+ "surfaces": {
5
+ "configKeys": {
6
+ "seed": 64,
7
+ "value": 64,
8
+ "decisions": []
9
+ },
10
+ "sections": {
11
+ "seed": 15,
12
+ "value": 15,
13
+ "decisions": []
14
+ },
15
+ "tools": {
16
+ "seed": 27,
17
+ "value": 27,
18
+ "decisions": []
19
+ }
20
+ }
21
+ }