Skip to content

Commit 688b1a5

Browse files
committed
chore(gates): point the daemon modularity gates at the split modules
R7 now locates the `SessionState` declaration by the declaration itself rather than by a recorded path: `sessionStateWritePressure` measures the merge-base tree too, and that tree still declares it in `daemon/types.ts` — a path constant would measure it as zero pressure and bank the headroom. R10's external-importer ratchet covers all three modules that replaced `daemon/types.ts`, so moving a symbol between them cannot reopen the boundary to a new outside zone. The recorded membership is unchanged: `client-normalizers.ts` and `remote/daemon-artifacts.ts` both import `daemon-request.ts` only. The `live-state-shape` and session-resource declaration sites move with `SessionState`; the depgraph lookalike fixture takes a new plausible path now that `daemon/session-state.ts` is the real root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ujrc8LYmvM249WY8921J1Y
1 parent ef14a7d commit 688b1a5

10 files changed

Lines changed: 95 additions & 50 deletions

scripts/depgraph/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ roots, exports, and named live-state symbols in `scripts/layering/architecture-o
120120

121121
- `vocabulary` — the target is a declared contract facade root.
122122
- `capability` — the target is a declared capability root and the import names a declared export.
123-
- `live-state-shape` — the edge names the exact `SessionState` type from `src/daemon/types.ts`.
123+
- `live-state-shape` — the edge names the exact `SessionState` type from `src/daemon/session-state.ts`.
124124
- `live-state-authority` — the edge names the exact `SessionStore` class from
125125
`src/daemon/session-store.ts`.
126126
- `executable-policy` — the source is under a declared executable-policy root.

scripts/depgraph/model.test.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,26 +42,26 @@ function authorityFixture(): Map<string, string> {
4242
"import { isSessionRecording } from './session-script-publication-capability.ts';",
4343
].join('\n'),
4444
'src/daemon/state-consumer.ts': [
45-
"import type { SessionState } from './types.ts';",
45+
"import type { SessionState } from './session-state.ts';",
4646
"import { SessionStore } from './session-store.ts';",
4747
].join('\n'),
4848
'src/daemon/type-consumer.ts': "import type { SessionStore } from './session-store.ts';\n",
4949
'src/snapshot/policy-consumer.ts': [
50-
"import type { SessionState } from '../daemon/types.ts';",
51-
"import type { SessionRef } from '../daemon/types.ts';",
50+
"import type { SessionState } from '../daemon/session-state.ts';",
51+
"import type { SessionRef } from '../daemon/session-state.ts';",
5252
"import './ordinary-target.ts';",
5353
].join('\n'),
5454
'src/daemon/ordinary-consumer.ts': [
55-
"import { SessionState } from './session-state.ts';",
55+
"import { SessionState } from './session-state-store.ts';",
5656
"import { createRequestRuntimeBindingsExtra } from './request-runtime-binding.ts';",
5757
].join('\n'),
58-
'src/daemon/types.ts':
58+
'src/daemon/session-state.ts':
5959
'export type SessionState = { name: string };\nexport type SessionRef = unknown;\n',
6060
'src/daemon/session-store.ts': 'export class SessionStore {}\n',
6161
'src/daemon/request-runtime-binding.ts': 'export function createRequestRuntimeBindings() {}\n',
6262
'src/daemon/session-script-publication-capability.ts':
6363
'export function isSessionRecording() {}\n',
64-
'src/daemon/session-state.ts': 'export const SessionState = 1;\n',
64+
'src/daemon/session-state-store.ts': 'export const SessionState = 1;\n',
6565
'src/snapshot/ordinary-target.ts': 'export const ordinary = 1;\n',
6666
'packages/contracts/src/facades/client.ts': 'export type ClientShape = string;\n',
6767
'packages/contracts/src/facades/capture.ts': 'export type CaptureShape = string;\n',
@@ -92,10 +92,13 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an
9292
graphEdge(graph, 'src/daemon/capability-consumer.ts', 'src/daemon/request-runtime-binding.ts'),
9393
{ kind: 'value', labels: ['capability'] },
9494
);
95-
assert.deepEqual(graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/types.ts'), {
96-
kind: 'type',
97-
labels: ['live-state-shape'],
98-
});
95+
assert.deepEqual(
96+
graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/session-state.ts'),
97+
{
98+
kind: 'type',
99+
labels: ['live-state-shape'],
100+
},
101+
);
99102
assert.deepEqual(
100103
graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/session-store.ts'),
101104
{ kind: 'value', labels: ['live-state-authority'] },
@@ -109,7 +112,7 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an
109112
{ kind: 'value', labels: ['executable-policy'] },
110113
);
111114
assert.deepEqual(
112-
graphEdge(graph, 'src/daemon/ordinary-consumer.ts', 'src/daemon/session-state.ts'),
115+
graphEdge(graph, 'src/daemon/ordinary-consumer.ts', 'src/daemon/session-state-store.ts'),
113116
{ kind: 'value', labels: ['ordinary'] },
114117
);
115118
assert.deepEqual(
@@ -119,13 +122,17 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an
119122

120123
const stateEdges = resolveImportEdges(files, authorityWorkspaceTargets()).filter(
121124
(edge) =>
122-
edge.file === 'src/snapshot/policy-consumer.ts' && edge.target === 'src/daemon/types.ts',
125+
edge.file === 'src/snapshot/policy-consumer.ts' &&
126+
edge.target === 'src/daemon/session-state.ts',
123127
);
124128
assert.equal(stateEdges.length, 2, 'the fixture must exercise raw same-pair imports');
125-
assert.deepEqual(graphEdge(graph, 'src/snapshot/policy-consumer.ts', 'src/daemon/types.ts'), {
126-
kind: 'type',
127-
labels: ['live-state-shape', 'executable-policy'],
128-
});
129+
assert.deepEqual(
130+
graphEdge(graph, 'src/snapshot/policy-consumer.ts', 'src/daemon/session-state.ts'),
131+
{
132+
kind: 'type',
133+
labels: ['live-state-shape', 'executable-policy'],
134+
},
135+
);
129136
assert.deepEqual(graph.edgeAuthorities.length, graph.edges.length);
130137
assert.deepEqual(Object.keys(graph.authorityCounts), AUTHORITY_LABELS);
131138
assert.deepEqual(graph.authorityCounts, {
@@ -163,8 +170,8 @@ test('live-state labels follow shared declarations and reject lookalike targets'
163170
assert.deepEqual(
164171
authorityLabelsForEdge({
165172
file: 'src/core/live-state-consumer.ts',
166-
target: 'src/daemon/session-state.ts',
167-
spec: './session-state.ts',
173+
target: 'src/daemon/session-state-store.ts',
174+
spec: './session-state-store.ts',
168175
dynamic: false,
169176
typeOnly: true,
170177
line: 1,

scripts/layering/architecture-ownership.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const ARCHITECTURE_OWNERSHIP = {
221221
{
222222
name: 'session-state-shape',
223223
kind: 'live-state-shape',
224-
root: 'src/daemon/types.ts',
224+
root: 'src/daemon/session-state.ts',
225225
exports: ['SessionState'],
226226
},
227227
{

scripts/layering/check.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
// merge-base (R9). R4 keeps the value graph acyclic, so these cycles are free at
2727
// runtime but bound what can be read in isolation.
2828
// - Across the DAEMON MODULARITY MIGRATION: R7 ownership pressure and external
29-
// daemon/types.ts importers only shrink, R9 zone membership cannot grow or absorb
29+
// daemon request/session-state importers only shrink, R9 zone membership cannot grow or absorb
3030
// engine files, and planned logical modules start with zero forbidden/internal imports (R10).
3131
// - Over the WORKSPACE PACKAGES: no root back-imports, no relative tunnelling past
3232
// an exports map, and every workspace specifier declared + exports-named (R11).
@@ -50,6 +50,7 @@ import { pathToFileURL } from 'node:url';
5050
import {
5151
fieldClassificationDrift,
5252
findSessionStateWrites,
53+
sessionStateDeclarationFile,
5354
sessionStateFields,
5455
sessionStateFieldCount,
5556
SESSION_STATE_FIELD_OWNERS,
@@ -233,19 +234,19 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
233234
}
234235

235236
function checkSessionStateOwnership(sources: ReadonlyMap<string, string>): LayeringViolation[] {
236-
const types = sources.get('src/daemon/types.ts');
237-
if (!types) {
237+
const declarationFile = sessionStateDeclarationFile(sources);
238+
if (!declarationFile) {
238239
return [
239240
{
240241
rule: 'R7 session-state-ownership',
241-
file: 'src/daemon/types.ts',
242+
file: 'src/daemon/session-state.ts',
242243
line: 1,
243-
message: 'daemon/types.ts is missing, so SessionState ownership cannot be checked.',
244+
message: 'no daemon module declares SessionState, so its ownership cannot be checked.',
244245
},
245246
];
246247
}
247248

248-
const fields = sessionStateFields(types);
249+
const fields = sessionStateFields(sources.get(declarationFile)!);
249250
const writes = findSessionStateWrites(sources, fields);
250251
const violations: LayeringViolation[] = [];
251252
const seenOwners = new Map<string, Set<string>>();

scripts/layering/daemon-modularity.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function importEdge(file: string, target: string): ResolvedImportEdge {
2727

2828
function baselineDaemonTypesEdges(): ResolvedImportEdge[] {
2929
return DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.map((file) =>
30-
importEdge(file, 'src/daemon/types.ts'),
30+
importEdge(file, 'src/daemon/daemon-request.ts'),
3131
);
3232
}
3333

@@ -105,11 +105,14 @@ test('R10 banks an R7 shrink with no edit anywhere', () => {
105105
);
106106
});
107107

108-
test('external daemon/types.ts importer membership changes require the baseline to change', () => {
108+
test('external daemon request/session-state importer membership changes require the baseline to change', () => {
109109
const edges = resolveImportEdges(
110110
new Map([
111-
['src/client/new-importer.ts', "import type { DaemonRequest } from '../daemon/types.ts';"],
112-
['src/daemon/types.ts', 'export type DaemonRequest = { command: string };'],
111+
[
112+
'src/client/new-importer.ts',
113+
"import type { SessionState } from '../daemon/session-state.ts';",
114+
],
115+
['src/daemon/session-state.ts', 'export type SessionState = { name: string };'],
113116
]),
114117
);
115118

scripts/layering/daemon-modularity.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ export const DAEMON_MODULARITY_BASELINE = {
2121
],
2222
} as const;
2323

24+
// The modules that own the daemon's dispatch vocabulary since #2338 split `daemon/types.ts`:
25+
// the request shape, its wire-only half, and the live session record. All three are ratcheted
26+
// together, so moving a symbol between them cannot reopen the boundary to a new outside zone.
27+
const DAEMON_TYPE_MODULES: readonly string[] = [
28+
'src/daemon/daemon-request.ts',
29+
'src/daemon/daemon-request-wire.ts',
30+
'src/daemon/session-state.ts',
31+
];
32+
2433
const ENGINE_FILE_PREFIXES = [
2534
'packages/ad-replay/src/',
2635
'packages/maestro/src/',
@@ -31,15 +40,15 @@ const ENGINE_FILE_PREFIXES = [
3140
/**
3241
* Catches: the daemon modularity migration regressing quietly — a SessionState field losing
3342
* its owner, a logical module gaining a forbidden or internal import, or an external
34-
* daemon/types.ts importer count creeping up — any of which erodes the wave-by-wave
43+
* external daemon request/session-state importer count creeping up — any of which erodes the wave-by-wave
3544
* extraction #1478/#1478-P5 already paid for, and nothing enforces the wave order itself.
3645
* Evidence: 2316fd32c5 (#1487) pinned the migration contracts this ratchet grew from;
3746
* 6984a1e095 (#1852) fixed the R10 zone-listing message when the type-cycle ceiling trips.
3847
* Cost: 937 LOC total for the file (323 rule + 614 test; shared with R9's checkTypeCycleBaseline
3948
* below, not attributed separately).
4049
* Kill criterion: none enforced today; retire only by maintainer decision that the daemon
4150
* modularity measurements (SessionState field-owner counts, logical-module import policies and
42-
* facades, the external daemon/types.ts importer list, per-zone cycle membership) no longer
51+
* facades, the external daemon request/session-state importer list, per-zone cycle membership) no longer
4352
* matter. Every one is a count or an import edge the compiler accepts either way.
4453
*/
4554
export function checkDaemonModularityRatchets(
@@ -204,7 +213,7 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
204213
const allowed = new Set<string>(DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters);
205214
const importers = new Map<string, ResolvedImportEdge>();
206215
for (const edge of edges) {
207-
if (edge.target !== 'src/daemon/types.ts' || edge.file.startsWith('src/daemon/')) continue;
216+
if (!DAEMON_TYPE_MODULES.includes(edge.target) || edge.file.startsWith('src/daemon/')) continue;
208217
importers.set(edge.file, edge);
209218
}
210219
const violations = [...importers]
@@ -214,7 +223,8 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
214223
file,
215224
line: edge.line,
216225
message:
217-
`external production imports of daemon/types.ts may only shrink from the recorded ${allowed.size}. ` +
226+
`external production imports of the daemon request/session-state modules may only shrink ` +
227+
`from the recorded ${allowed.size}. ` +
218228
'Use an existing neutral contract; do not move DaemonRequest into contracts to satisfy this gate.',
219229
}));
220230
for (const file of allowed) {
@@ -223,7 +233,7 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
223233
rule: 'R10 daemon-modularity',
224234
file: 'scripts/layering/daemon-modularity.ts',
225235
line: 1,
226-
message: `${file} no longer imports daemon/types.ts — delete it from externalDaemonTypesImporters in the same change so the dependency cannot return.`,
236+
message: `${file} no longer imports a daemon request/session-state module — delete it from externalDaemonTypesImporters in the same change so the dependency cannot return.`,
227237
});
228238
}
229239
return violations;
@@ -309,7 +319,7 @@ export function daemonModularitySummary(reference: LayeringRatchets): string {
309319
return (
310320
`R10 holds R7 at the merge-base's ${session.writerOwnedFields} writer-owned fields / ` +
311321
`${session.ownerFileClaims} owner claims, R9 at its ${reference.largestTypeCycle.length} files per zone, ` +
312-
`${DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.length} external daemon/types.ts importers, ` +
322+
`${DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.length} external daemon request/session-state importers, ` +
313323
'and zero forbidden logical-module imports'
314324
);
315325
}

scripts/layering/model.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ test('every SessionState field is classified exactly once', () => {
414414
// R7 by being invisible to the scan, and the rule would silently stop covering part of the
415415
// type it claims to cover.
416416
const fields = sessionStateFields(
417-
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/types.ts'), 'utf8'),
417+
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/session-state.ts'), 'utf8'),
418418
);
419419
assert.deepEqual(fieldClassificationDrift(fields), []);
420420
assert.equal(
@@ -425,7 +425,7 @@ test('every SessionState field is classified exactly once', () => {
425425

426426
test('classification drift is reported in all three directions', () => {
427427
const declared = sessionStateFields(
428-
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/types.ts'), 'utf8'),
428+
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/session-state.ts'), 'utf8'),
429429
);
430430

431431
// Unclassified: a field added to SessionState and to neither table. This is the case the

scripts/layering/ratchet-reference.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const SESSION_TYPES = [
1717

1818
function tree(extra: Record<string, string> = {}) {
1919
return new Map<string, string>([
20-
['src/daemon/types.ts', SESSION_TYPES],
20+
['src/daemon/session-state.ts', SESSION_TYPES],
2121
['src/daemon/session-snapshot.ts', 'session.snapshot = "a"; nextSession.snapshot = "b";'],
2222
['src/daemon/ref-frame.ts', 'session.snapshot = undefined;'],
2323
['src/daemon/handlers/trace-runtime.ts', 'session.trace ??= "t";'],

scripts/layering/session-resource-ownership.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ type AstNode = Record<string, unknown>;
1818
export const SESSION_RESOURCE_OWNERSHIP_RULE = 'R68 session-resource-ownership';
1919

2020
const RESOURCE_OWNERS: Readonly<Record<string, ReadonlySet<string>>> = {
21-
appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/types.ts']),
22-
appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/types.ts']),
23-
audioProbe: new Set(['src/daemon/audio-probe-session-resource.ts', 'src/daemon/types.ts']),
24-
perfCapture: new Set(['src/daemon/perf-capture-session-resource.ts', 'src/daemon/types.ts']),
21+
appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']),
22+
appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']),
23+
audioProbe: new Set([
24+
'src/daemon/audio-probe-session-resource.ts',
25+
'src/daemon/session-state.ts',
26+
]),
27+
perfCapture: new Set([
28+
'src/daemon/perf-capture-session-resource.ts',
29+
'src/daemon/session-state.ts',
30+
]),
2531
};
2632

2733
/** Durable session-resource records have one whole-record construction owner per domain. */

scripts/layering/session-state.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,30 @@ export function fieldClassificationDrift(fields: readonly string[]): FieldClassi
166166
return drift.sort((left, right) => left.field.localeCompare(right.field));
167167
}
168168

169+
const SESSION_STATE_DECLARATION = /export type SessionState = \{([\s\S]*?)\n\};/;
170+
171+
/**
172+
* The daemon module that declares `SessionState`, found by the declaration rather than by a
173+
* recorded path. `sessionStateWritePressure` below measures the merge-base tree with the same
174+
* function, and that tree's declaration may still sit where this tree no longer has it — a
175+
* path constant would silently measure such a tree as zero pressure and bank the headroom.
176+
*/
177+
export function sessionStateDeclarationFile(
178+
sources: ReadonlyMap<string, string>,
179+
): string | undefined {
180+
for (const [file, source] of sources) {
181+
if (file.startsWith('src/daemon/') && SESSION_STATE_DECLARATION.test(source)) return file;
182+
}
183+
return undefined;
184+
}
185+
169186
/**
170187
* Field names declared by `SessionState` itself, so the scan cannot be fooled by a daemon
171188
* module with an unrelated local named `session` (a provider session, a runner session).
172189
*/
173190
export function sessionStateFields(typesSource: string): string[] {
174-
const declaration = /export type SessionState = \{([\s\S]*?)\n\};/.exec(typesSource);
175-
if (!declaration) throw new Error('SessionState declaration not found in daemon/types.ts');
191+
const declaration = SESSION_STATE_DECLARATION.exec(typesSource);
192+
if (!declaration) throw new Error('SessionState declaration not found');
176193
return [...declaration[1]!.matchAll(/^ {2}([a-zA-Z][A-Za-z0-9]*)\??:/gm)].map(
177194
(match) => match[1]!,
178195
);
@@ -294,11 +311,12 @@ export type SessionStateWritePressure = Readonly<{
294311
export function sessionStateWritePressure(
295312
sources: ReadonlyMap<string, string>,
296313
): SessionStateWritePressure {
297-
const types = sources.get('src/daemon/types.ts');
298-
if (!types) return { writerOwnedFields: 0, ownerFileClaims: 0 };
299-
const writes = findSessionStateWrites(sources, sessionStateFields(types)).filter(
300-
(write) => write.field !== '[computed]',
301-
);
314+
const declarationFile = sessionStateDeclarationFile(sources);
315+
if (!declarationFile) return { writerOwnedFields: 0, ownerFileClaims: 0 };
316+
const writes = findSessionStateWrites(
317+
sources,
318+
sessionStateFields(sources.get(declarationFile)!),
319+
).filter((write) => write.field !== '[computed]');
302320
return {
303321
writerOwnedFields: new Set(writes.map((write) => write.field)).size,
304322
ownerFileClaims: new Set(writes.map((write) => `${write.field}\0${write.file}`)).size,

0 commit comments

Comments
 (0)