@adhdev/mesh-shared 1.0.60-rc.9 → 1.0.61-rc.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/dist/brain-routing.d.ts +6 -8
- package/dist/git-diff.d.ts +21 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +2876 -77
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2625 -72
- package/dist/index.mjs.map +1 -1
- package/dist/interpolation.d.ts +0 -9
- package/dist/json.d.ts +4 -0
- package/dist/keyed-doc-delta.d.ts +105 -0
- package/dist/mesh-tool-names.d.ts +31 -2
- package/dist/mesh-vocabulary.d.ts +66 -0
- package/dist/mission-brief.d.ts +62 -0
- package/dist/outbound-message.d.ts +201 -0
- package/dist/path-ownership.d.ts +101 -0
- package/dist/protocol/daemon-server.d.ts +356 -0
- package/dist/protocol/dashboard-daemon-p2p.d.ts +213 -0
- package/dist/protocol/dashboard-server.d.ts +240 -0
- package/dist/protocol/dashboard-wire-version.d.ts +21 -0
- package/dist/protocol/envelope.d.ts +55 -0
- package/dist/protocol/index.d.ts +16 -0
- package/dist/session-launch.d.ts +105 -0
- package/dist/session-status.d.ts +75 -0
- package/dist/slot-proposal.d.ts +2 -28
- package/dist/turn-evidence.d.ts +434 -0
- package/dist/turn-ipc-guards.d.ts +18 -0
- package/dist/turn-ipc-mission.d.ts +250 -0
- package/dist/turn-ipc-queue.d.ts +196 -0
- package/dist/turn-ipc-stats.d.ts +72 -0
- package/dist/turn-ipc-turn.d.ts +176 -0
- package/dist/turn-ipc.d.ts +90 -0
- package/dist/types.d.ts +11 -0
- package/dist/worker-protocol.d.ts +69 -0
- package/dist/ws-protocol.d.ts +69 -41
- package/package.json +1 -1
- package/src/brain-routing.ts +9 -11
- package/src/git-diff.ts +48 -0
- package/src/index.ts +12 -1
- package/src/interpolation.ts +0 -18
- package/src/json.ts +10 -0
- package/src/keyed-doc-delta.ts +413 -0
- package/src/mesh-tool-names.ts +66 -25
- package/src/mesh-vocabulary.ts +94 -0
- package/src/mission-brief.ts +153 -0
- package/src/outbound-message.ts +319 -0
- package/src/path-ownership.ts +271 -0
- package/src/protocol/daemon-server.ts +510 -0
- package/src/protocol/dashboard-daemon-p2p.ts +273 -0
- package/src/protocol/dashboard-server.ts +340 -0
- package/src/protocol/dashboard-wire-version.ts +27 -0
- package/src/protocol/envelope.ts +75 -0
- package/src/protocol/index.ts +17 -0
- package/src/session-launch.ts +245 -0
- package/src/session-status.ts +164 -0
- package/src/slot-proposal.ts +2 -48
- package/src/turn-evidence.ts +596 -0
- package/src/turn-ipc-guards.ts +61 -0
- package/src/turn-ipc-mission.ts +647 -0
- package/src/turn-ipc-queue.ts +493 -0
- package/src/turn-ipc-stats.ts +193 -0
- package/src/turn-ipc-turn.ts +453 -0
- package/src/turn-ipc.ts +137 -0
- package/src/types.ts +8 -0
- package/src/worker-protocol.ts +164 -0
- package/src/ws-protocol.ts +98 -93
- package/dist/magi.d.ts +0 -281
- package/src/magi.ts +0 -310
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-ownership — declared file-ownership sets for `code_change` mesh tasks
|
|
3
|
+
* (wiring-unification Phase H1, `docs/design/2026-09-23-wiring-unification.md`
|
|
4
|
+
* §7c).
|
|
5
|
+
*
|
|
6
|
+
* Today two `code_change` tasks on two different nodes (two worktrees of the
|
|
7
|
+
* same branch, or main + a worktree) can silently touch the same file with
|
|
8
|
+
* zero warning — the only existing isolation is node-level ("one active write
|
|
9
|
+
* task per node"), never path-level. This module is the pure, dependency-free
|
|
10
|
+
* core of the fix: normalizing a caller-declared `owned_paths` list, and
|
|
11
|
+
* comparing declared sets for overlap. It has NO opinion on enforcement policy
|
|
12
|
+
* (refuse vs warn, which axis a caller compares against) — that is the mesh
|
|
13
|
+
* runtime's job (`claimNextTask` / `claimNextQueueTask` in daemon-core); this
|
|
14
|
+
* module only answers "do these two declarations conflict" and "did the
|
|
15
|
+
* touched-files report stay inside the declaration".
|
|
16
|
+
*
|
|
17
|
+
* SCOPE DECISION (glob support): minimal, deliberately. A declaration entry is
|
|
18
|
+
* either an exact repo-relative path (`src/foo.ts`) or a directory prefix
|
|
19
|
+
* spelled with a trailing `/**` (`src/mesh/**`) meaning "this path and
|
|
20
|
+
* everything under it". No `*`/`?` mid-segment wildcards, no `!` negation, no
|
|
21
|
+
* brace expansion. Two `code_change` tasks racing on overlapping files is a
|
|
22
|
+
* coordination bug to catch, not a general glob engine to build — a full
|
|
23
|
+
* minimatch-style matcher would need its own test suite and still leave the
|
|
24
|
+
* same ambiguity (does `src/*.ts` "contain" `src/foo.ts`?) the design doc's
|
|
25
|
+
* risk list calls out. `src/` (no `**`) is treated as an exact single-path
|
|
26
|
+
* entry, i.e. it owns a literal file/dir node named `src`, NOT everything
|
|
27
|
+
* under it — callers who want subtree ownership must write `src/**`.
|
|
28
|
+
*
|
|
29
|
+
* Rejected inputs (absolute paths, `..` segments, backslash-only entries that
|
|
30
|
+
* still resolve outside the repo after normalization) are dropped rather than
|
|
31
|
+
* silently coerced — `normalizeOwnedPaths` reports what it dropped so the
|
|
32
|
+
* caller (enqueue-time validation) can decide whether to warn or refuse.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** One normalized ownership entry: a repo-relative path, optionally a `**` subtree prefix. */
|
|
36
|
+
export interface OwnedPathEntry {
|
|
37
|
+
/** Repo-relative path with forward slashes, no leading `./`, no trailing slash. */
|
|
38
|
+
readonly path: string
|
|
39
|
+
/** True when this entry was declared as `<path>/**` (owns the whole subtree). */
|
|
40
|
+
readonly subtree: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A caller's normalized ownership declaration. */
|
|
44
|
+
export interface OwnedPathsDeclaration {
|
|
45
|
+
readonly paths: readonly OwnedPathEntry[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface NormalizeOwnedPathsResult {
|
|
49
|
+
readonly declaration: OwnedPathsDeclaration
|
|
50
|
+
/** Raw input entries that were dropped, with why — surfaced to the caller, never silently coerced. */
|
|
51
|
+
readonly rejected: ReadonlyArray<{ input: string; reason: string }>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Cap mirrors `depends_on`'s implicit list-size discipline (design doc H1 proposal). */
|
|
55
|
+
export const MAX_OWNED_PATHS = 64
|
|
56
|
+
|
|
57
|
+
function toPosix(raw: string): string {
|
|
58
|
+
return raw.replace(/\\/g, '/')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize one raw entry. Returns `null` (with a reason pushed to `rejected`)
|
|
63
|
+
* for anything absolute, escaping the repo via `..`, or empty after trimming.
|
|
64
|
+
*/
|
|
65
|
+
function normalizeOne(raw: unknown, rejected: Array<{ input: string; reason: string }>): OwnedPathEntry | null {
|
|
66
|
+
if (typeof raw !== 'string') {
|
|
67
|
+
rejected.push({ input: String(raw), reason: 'not_a_string' })
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const trimmed = raw.trim()
|
|
71
|
+
if (!trimmed) {
|
|
72
|
+
rejected.push({ input: raw, reason: 'empty' })
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
let posix = toPosix(trimmed)
|
|
76
|
+
|
|
77
|
+
// Absolute paths (POSIX `/…`, Windows drive letter `C:\…` or `C:/…`, or a
|
|
78
|
+
// stray leading `~`) are refused outright — ownership is repo-relative by
|
|
79
|
+
// construction, and accepting an absolute path would let a declaration
|
|
80
|
+
// silently escape the workspace it is meant to scope.
|
|
81
|
+
if (posix.startsWith('/') || /^[a-zA-Z]:\//.test(posix) || posix.startsWith('~')) {
|
|
82
|
+
rejected.push({ input: raw, reason: 'absolute_path' })
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Strip a leading './'.
|
|
87
|
+
posix = posix.replace(/^\.\/+/, '')
|
|
88
|
+
|
|
89
|
+
// Reject any '..' segment anywhere — no escaping the repo root.
|
|
90
|
+
const segments = posix.split('/').filter(Boolean)
|
|
91
|
+
if (segments.some(seg => seg === '..')) {
|
|
92
|
+
rejected.push({ input: raw, reason: 'parent_traversal' })
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
if (segments.length === 0) {
|
|
96
|
+
rejected.push({ input: raw, reason: 'empty' })
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Minimal glob support: a trailing '**' segment (from '<path>/**') marks a
|
|
101
|
+
// subtree declaration. Any OTHER '*'/'?' anywhere is rejected — this module
|
|
102
|
+
// does not implement general glob matching (see module doc).
|
|
103
|
+
let subtree = false
|
|
104
|
+
if (segments[segments.length - 1] === '**') {
|
|
105
|
+
subtree = true
|
|
106
|
+
segments.pop()
|
|
107
|
+
if (segments.length === 0) {
|
|
108
|
+
// '**' alone (or './**') claims the entire repo — allowed, but the
|
|
109
|
+
// caller (enqueue-time validation) is expected to lint/warn on it
|
|
110
|
+
// per the design doc's deadlock risk; this module only normalizes.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (segments.some(seg => seg.includes('*') || seg.includes('?'))) {
|
|
114
|
+
rejected.push({ input: raw, reason: 'unsupported_glob' })
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { path: segments.join('/'), subtree }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Normalize a caller-supplied `owned_paths` array. Never throws — invalid
|
|
123
|
+
* entries are dropped and reported in `rejected`; duplicates (after
|
|
124
|
+
* normalization) collapse to one entry (subtree wins if either declaration
|
|
125
|
+
* form is present for the same path). Entries beyond `MAX_OWNED_PATHS` (after
|
|
126
|
+
* dedup) are dropped with reason `over_cap`, preserving declaration order.
|
|
127
|
+
*/
|
|
128
|
+
export function normalizeOwnedPaths(input: unknown): NormalizeOwnedPathsResult {
|
|
129
|
+
const rejected: Array<{ input: string; reason: string }> = []
|
|
130
|
+
if (input === undefined || input === null) {
|
|
131
|
+
return { declaration: { paths: [] }, rejected }
|
|
132
|
+
}
|
|
133
|
+
if (!Array.isArray(input)) {
|
|
134
|
+
return { declaration: { paths: [] }, rejected: [{ input: String(input), reason: 'not_an_array' }] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const byPath = new Map<string, OwnedPathEntry>()
|
|
138
|
+
for (const raw of input) {
|
|
139
|
+
const entry = normalizeOne(raw, rejected)
|
|
140
|
+
if (!entry) continue
|
|
141
|
+
const existing = byPath.get(entry.path)
|
|
142
|
+
if (!existing) {
|
|
143
|
+
byPath.set(entry.path, entry)
|
|
144
|
+
} else if (entry.subtree && !existing.subtree) {
|
|
145
|
+
byPath.set(entry.path, entry)
|
|
146
|
+
}
|
|
147
|
+
// else: duplicate, existing entry already at least as broad — keep it.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const all = [...byPath.values()]
|
|
151
|
+
const kept = all.slice(0, MAX_OWNED_PATHS)
|
|
152
|
+
for (const dropped of all.slice(MAX_OWNED_PATHS)) {
|
|
153
|
+
rejected.push({ input: dropped.path, reason: 'over_cap' })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { declaration: { paths: kept }, rejected }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when `path` is exactly `base`, or (when `base` is a subtree entry) nested under it. */
|
|
160
|
+
function entryContains(base: OwnedPathEntry, path: string): boolean {
|
|
161
|
+
if (path === base.path) return true
|
|
162
|
+
if (base.subtree && (path === base.path || path.startsWith(`${base.path}/`))) return true
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** True when two normalized entries' claimed sets intersect (exact match, or one contains the other's directory). */
|
|
167
|
+
function entriesOverlap(a: OwnedPathEntry, b: OwnedPathEntry): boolean {
|
|
168
|
+
if (a.path === b.path) return true
|
|
169
|
+
if (a.subtree && (b.path === a.path || b.path.startsWith(`${a.path}/`))) return true
|
|
170
|
+
if (b.subtree && (a.path === b.path || a.path.startsWith(`${b.path}/`))) return true
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** True when two declarations claim any overlapping path. Pure set comparison — no policy (refuse vs warn) here. */
|
|
175
|
+
export function pathsOverlap(a: OwnedPathsDeclaration, b: OwnedPathsDeclaration): boolean {
|
|
176
|
+
if (a.paths.length === 0 || b.paths.length === 0) return false
|
|
177
|
+
for (const pa of a.paths) {
|
|
178
|
+
for (const pb of b.paths) {
|
|
179
|
+
if (entriesOverlap(pa, pb)) return true
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface InFlightOwnership {
|
|
186
|
+
readonly taskId: string
|
|
187
|
+
readonly paths: OwnedPathsDeclaration
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface OwnershipConflict {
|
|
191
|
+
readonly taskId: string
|
|
192
|
+
/** The specific overlapping paths, for a readable refusal message. */
|
|
193
|
+
readonly overlappingPaths: readonly string[]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Compare a candidate declaration against every other in-flight task's
|
|
198
|
+
* declaration and return every conflict found (not just the first), each
|
|
199
|
+
* naming the owning task id and the specific overlapping path(s) — so a
|
|
200
|
+
* refusal message can say exactly what collided, not just that it did.
|
|
201
|
+
*
|
|
202
|
+
* Backward compatibility (design doc risk #4): an EMPTY candidate declaration
|
|
203
|
+
* never conflicts (opt-in only — absent `owned_paths` performs no overlap
|
|
204
|
+
* check for that task), and an in-flight entry with an empty declaration is
|
|
205
|
+
* never reported as a conflict source either.
|
|
206
|
+
*/
|
|
207
|
+
export function findOwnershipConflicts(
|
|
208
|
+
candidate: OwnedPathsDeclaration,
|
|
209
|
+
inFlight: readonly InFlightOwnership[],
|
|
210
|
+
): OwnershipConflict[] {
|
|
211
|
+
if (candidate.paths.length === 0) return []
|
|
212
|
+
const conflicts: OwnershipConflict[] = []
|
|
213
|
+
for (const other of inFlight) {
|
|
214
|
+
if (other.paths.paths.length === 0) continue
|
|
215
|
+
const overlappingPaths: string[] = []
|
|
216
|
+
for (const pa of candidate.paths) {
|
|
217
|
+
for (const pb of other.paths.paths) {
|
|
218
|
+
if (entriesOverlap(pa, pb)) {
|
|
219
|
+
const narrower = pa.path.length >= pb.path.length ? pa.path : pb.path
|
|
220
|
+
if (!overlappingPaths.includes(narrower)) overlappingPaths.push(narrower)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (overlappingPaths.length > 0) {
|
|
225
|
+
conflicts.push({ taskId: other.taskId, overlappingPaths })
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return conflicts
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface TouchedFilesOutsideOwnershipResult {
|
|
232
|
+
/** Touched files not covered by any declared owned-path entry. */
|
|
233
|
+
readonly undeclaredTouched: readonly string[]
|
|
234
|
+
/** Declared entries that no touched file matched (informational only — under-reporting is not flagged as a mismatch by policy, just visible here). */
|
|
235
|
+
readonly unusedDeclarations: readonly string[]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Compare a worker's `report_completion.touched_files` against its task's
|
|
240
|
+
* declared `owned_paths`. Pure comparison only — per the design doc, a
|
|
241
|
+
* mismatch is "surfaced as evidence, not silently accepted"; this function
|
|
242
|
+
* does not decide whether that is an error. Each `touched` entry is
|
|
243
|
+
* normalized with the same rules as a declaration (invalid entries — e.g. an
|
|
244
|
+
* absolute path a worker reported by mistake — are treated as undeclared
|
|
245
|
+
* rather than silently dropped, since they cannot match anything).
|
|
246
|
+
*/
|
|
247
|
+
export function touchedFilesOutsideOwnership(
|
|
248
|
+
touched: readonly string[],
|
|
249
|
+
owned: OwnedPathsDeclaration,
|
|
250
|
+
): TouchedFilesOutsideOwnershipResult {
|
|
251
|
+
const undeclaredTouched: string[] = []
|
|
252
|
+
const usedDeclarations = new Set<string>()
|
|
253
|
+
|
|
254
|
+
for (const raw of touched ?? []) {
|
|
255
|
+
const rejectedSink: Array<{ input: string; reason: string }> = []
|
|
256
|
+
const entry = normalizeOne(raw, rejectedSink)
|
|
257
|
+
if (!entry) {
|
|
258
|
+
undeclaredTouched.push(raw)
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
const matches = owned.paths.filter(base => entryContains(base, entry.path))
|
|
262
|
+
if (matches.length === 0) {
|
|
263
|
+
undeclaredTouched.push(entry.path)
|
|
264
|
+
} else {
|
|
265
|
+
for (const m of matches) usedDeclarations.add(m.path)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const unusedDeclarations = owned.paths.map(p => p.path).filter(p => !usedDeclarations.has(p))
|
|
270
|
+
return { undeclaredTouched, unusedDeclarations }
|
|
271
|
+
}
|