Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions packages/contracts/src/dispatched-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type { CommandFlags } from './command-flags.ts';
*
* - `kernel/contracts.ts` `DaemonRequest` — the WIRE shape, with `flags?: Record<string, unknown>`,
* because a process boundary cannot enforce a flag vocabulary.
* - `daemon/types.ts` `DaemonRequest` — the wire shape with `token`/`session` required, `flags`
* narrowed to `CommandFlags`, and `internal?: DaemonRequestInternal` carrying `SessionState`
* - `daemon/daemon-request.ts` `DaemonRequest` — `DaemonWireRequest` (`token`/`session` required,
* `flags` narrowed to `CommandFlags`) plus `internal?: DaemonRequestInternal` carrying `SessionState`
* callbacks, the admitted lease and the resolved session scope. Server-private, which is why it
* cannot move down here.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/contracts/src/session-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { MultiTargetAnnotationV1, TargetAnnotationV1 } from './target-annot
// One recorded action in a session's script.
//
// `replay/` reads and writes these and the Maestro package exports them, so declaring the
// shape inside `daemon/types.ts` made both zones depend on the daemon server to describe a file
// shape inside the daemon's own session record made both zones depend on the daemon server to
// describe a file
// format neither of them asks the daemon to produce. The daemon still owns the RECORDING — it is
// the only thing that appends actions; this is only the shape they are appended in.
//
Expand Down
2 changes: 1 addition & 1 deletion scripts/__tests__/help-conformance-sample-producers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { buildDeviceInUseBySessionError } from '../../src/daemon/session-recover
import { buildDeviceClaimConflictError } from '../../src/daemon/device-claim-conflict.ts';
import { activateCompleteRefFrame, readRefMutationFrame } from '../../src/daemon/ref-frame.ts';
import { resolveRefStalenessWarning } from '../../src/daemon/session-snapshot.ts';
import type { SessionState } from '../../src/daemon/types.ts';
import type { SessionState } from '../../src/daemon/session-state.ts';
import { buildAppNotInstalledError } from '@agent-device/platform-apple/app-resolution';
import {
presentConnectReadiness,
Expand Down
2 changes: 1 addition & 1 deletion scripts/depgraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ roots, exports, and named live-state symbols in `scripts/layering/architecture-o

- `vocabulary` — the target is a declared contract facade root.
- `capability` — the target is a declared capability root and the import names a declared export.
- `live-state-shape` — the edge names the exact `SessionState` type from `src/daemon/types.ts`.
- `live-state-shape` — the edge names the exact `SessionState` type from `src/daemon/session-state.ts`.
- `live-state-authority` — the edge names the exact `SessionStore` class from
`src/daemon/session-store.ts`.
- `executable-policy` — the source is under a declared executable-policy root.
Expand Down
43 changes: 25 additions & 18 deletions scripts/depgraph/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,26 @@ function authorityFixture(): Map<string, string> {
"import { isSessionRecording } from './session-script-publication-capability.ts';",
].join('\n'),
'src/daemon/state-consumer.ts': [
"import type { SessionState } from './types.ts';",
"import type { SessionState } from './session-state.ts';",
"import { SessionStore } from './session-store.ts';",
].join('\n'),
'src/daemon/type-consumer.ts': "import type { SessionStore } from './session-store.ts';\n",
'src/snapshot/policy-consumer.ts': [
"import type { SessionState } from '../daemon/types.ts';",
"import type { SessionRef } from '../daemon/types.ts';",
"import type { SessionState } from '../daemon/session-state.ts';",
"import type { SessionRef } from '../daemon/session-state.ts';",
"import './ordinary-target.ts';",
].join('\n'),
'src/daemon/ordinary-consumer.ts': [
"import { SessionState } from './session-state.ts';",
"import { SessionState } from './session-state-store.ts';",
"import { createRequestRuntimeBindingsExtra } from './request-runtime-binding.ts';",
].join('\n'),
'src/daemon/types.ts':
'src/daemon/session-state.ts':
'export type SessionState = { name: string };\nexport type SessionRef = unknown;\n',
'src/daemon/session-store.ts': 'export class SessionStore {}\n',
'src/daemon/request-runtime-binding.ts': 'export function createRequestRuntimeBindings() {}\n',
'src/daemon/session-script-publication-capability.ts':
'export function isSessionRecording() {}\n',
'src/daemon/session-state.ts': 'export const SessionState = 1;\n',
'src/daemon/session-state-store.ts': 'export const SessionState = 1;\n',
'src/snapshot/ordinary-target.ts': 'export const ordinary = 1;\n',
'packages/contracts/src/facades/client.ts': 'export type ClientShape = string;\n',
'packages/contracts/src/facades/capture.ts': 'export type CaptureShape = string;\n',
Expand Down Expand Up @@ -92,10 +92,13 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an
graphEdge(graph, 'src/daemon/capability-consumer.ts', 'src/daemon/request-runtime-binding.ts'),
{ kind: 'value', labels: ['capability'] },
);
assert.deepEqual(graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/types.ts'), {
kind: 'type',
labels: ['live-state-shape'],
});
assert.deepEqual(
graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/session-state.ts'),
{
kind: 'type',
labels: ['live-state-shape'],
},
);
assert.deepEqual(
graphEdge(graph, 'src/daemon/state-consumer.ts', 'src/daemon/session-store.ts'),
{ kind: 'value', labels: ['live-state-authority'] },
Expand All @@ -109,7 +112,7 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an
{ kind: 'value', labels: ['executable-policy'] },
);
assert.deepEqual(
graphEdge(graph, 'src/daemon/ordinary-consumer.ts', 'src/daemon/session-state.ts'),
graphEdge(graph, 'src/daemon/ordinary-consumer.ts', 'src/daemon/session-state-store.ts'),
{ kind: 'value', labels: ['ordinary'] },
);
assert.deepEqual(
Expand All @@ -119,13 +122,17 @@ test('authority overlay uses declared roots and symbols, keeps kind separate, an

const stateEdges = resolveImportEdges(files, authorityWorkspaceTargets()).filter(
(edge) =>
edge.file === 'src/snapshot/policy-consumer.ts' && edge.target === 'src/daemon/types.ts',
edge.file === 'src/snapshot/policy-consumer.ts' &&
edge.target === 'src/daemon/session-state.ts',
);
assert.equal(stateEdges.length, 2, 'the fixture must exercise raw same-pair imports');
assert.deepEqual(graphEdge(graph, 'src/snapshot/policy-consumer.ts', 'src/daemon/types.ts'), {
kind: 'type',
labels: ['live-state-shape', 'executable-policy'],
});
assert.deepEqual(
graphEdge(graph, 'src/snapshot/policy-consumer.ts', 'src/daemon/session-state.ts'),
{
kind: 'type',
labels: ['live-state-shape', 'executable-policy'],
},
);
assert.deepEqual(graph.edgeAuthorities.length, graph.edges.length);
assert.deepEqual(Object.keys(graph.authorityCounts), AUTHORITY_LABELS);
assert.deepEqual(graph.authorityCounts, {
Expand Down Expand Up @@ -163,8 +170,8 @@ test('live-state labels follow shared declarations and reject lookalike targets'
assert.deepEqual(
authorityLabelsForEdge({
file: 'src/core/live-state-consumer.ts',
target: 'src/daemon/session-state.ts',
spec: './session-state.ts',
target: 'src/daemon/session-state-store.ts',
spec: './session-state-store.ts',
dynamic: false,
typeOnly: true,
line: 1,
Expand Down
2 changes: 1 addition & 1 deletion scripts/layering/architecture-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export const ARCHITECTURE_OWNERSHIP = {
{
name: 'session-state-shape',
kind: 'live-state-shape',
root: 'src/daemon/types.ts',
root: 'src/daemon/session-state.ts',
exports: ['SessionState'],
},
{
Expand Down
13 changes: 7 additions & 6 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
// merge-base (R9). R4 keeps the value graph acyclic, so these cycles are free at
// runtime but bound what can be read in isolation.
// - Across the DAEMON MODULARITY MIGRATION: R7 ownership pressure and external
// daemon/types.ts importers only shrink, R9 zone membership cannot grow or absorb
// daemon request/session-state importers only shrink, R9 zone membership cannot grow or absorb
// engine files, and planned logical modules start with zero forbidden/internal imports (R10).
// - Over the WORKSPACE PACKAGES: no root back-imports, no relative tunnelling past
// an exports map, and every workspace specifier declared + exports-named (R11).
Expand All @@ -50,6 +50,7 @@ import { pathToFileURL } from 'node:url';
import {
fieldClassificationDrift,
findSessionStateWrites,
sessionStateDeclarationFile,
sessionStateFields,
sessionStateFieldCount,
SESSION_STATE_FIELD_OWNERS,
Expand Down Expand Up @@ -233,19 +234,19 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
}

function checkSessionStateOwnership(sources: ReadonlyMap<string, string>): LayeringViolation[] {
const types = sources.get('src/daemon/types.ts');
if (!types) {
const declarationFile = sessionStateDeclarationFile(sources);
if (!declarationFile) {
return [
{
rule: 'R7 session-state-ownership',
file: 'src/daemon/types.ts',
file: 'src/daemon/session-state.ts',
line: 1,
message: 'daemon/types.ts is missing, so SessionState ownership cannot be checked.',
message: 'no daemon module declares SessionState, so its ownership cannot be checked.',
},
];
}

const fields = sessionStateFields(types);
const fields = sessionStateFields(sources.get(declarationFile)!);
const writes = findSessionStateWrites(sources, fields);
const violations: LayeringViolation[] = [];
const seenOwners = new Map<string, Set<string>>();
Expand Down
11 changes: 7 additions & 4 deletions scripts/layering/daemon-modularity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function importEdge(file: string, target: string): ResolvedImportEdge {

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

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

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

Expand Down
22 changes: 16 additions & 6 deletions scripts/layering/daemon-modularity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export const DAEMON_MODULARITY_BASELINE = {
],
} as const;

// The modules that own the daemon's dispatch vocabulary since #2338 split `daemon/types.ts`:
// the request shape, its wire-only half, and the live session record. All three are ratcheted
// together, so moving a symbol between them cannot reopen the boundary to a new outside zone.
const DAEMON_TYPE_MODULES: readonly string[] = [
'src/daemon/daemon-request.ts',
'src/daemon/daemon-request-wire.ts',
'src/daemon/session-state.ts',
];

const ENGINE_FILE_PREFIXES = [
'packages/ad-replay/src/',
'packages/maestro/src/',
Expand All @@ -31,15 +40,15 @@ const ENGINE_FILE_PREFIXES = [
/**
* Catches: the daemon modularity migration regressing quietly — a SessionState field losing
* its owner, a logical module gaining a forbidden or internal import, or an external
* daemon/types.ts importer count creeping up — any of which erodes the wave-by-wave
* external daemon request/session-state importer count creeping up — any of which erodes the wave-by-wave
* extraction #1478/#1478-P5 already paid for, and nothing enforces the wave order itself.
* Evidence: 2316fd32c5 (#1487) pinned the migration contracts this ratchet grew from;
* 6984a1e095 (#1852) fixed the R10 zone-listing message when the type-cycle ceiling trips.
* Cost: 937 LOC total for the file (323 rule + 614 test; shared with R9's checkTypeCycleBaseline
* below, not attributed separately).
* Kill criterion: none enforced today; retire only by maintainer decision that the daemon
* modularity measurements (SessionState field-owner counts, logical-module import policies and
* facades, the external daemon/types.ts importer list, per-zone cycle membership) no longer
* facades, the external daemon request/session-state importer list, per-zone cycle membership) no longer
* matter. Every one is a count or an import edge the compiler accepts either way.
*/
export function checkDaemonModularityRatchets(
Expand Down Expand Up @@ -204,7 +213,7 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
const allowed = new Set<string>(DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters);
const importers = new Map<string, ResolvedImportEdge>();
for (const edge of edges) {
if (edge.target !== 'src/daemon/types.ts' || edge.file.startsWith('src/daemon/')) continue;
if (!DAEMON_TYPE_MODULES.includes(edge.target) || edge.file.startsWith('src/daemon/')) continue;
importers.set(edge.file, edge);
}
const violations = [...importers]
Expand All @@ -214,7 +223,8 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
file,
line: edge.line,
message:
`external production imports of daemon/types.ts may only shrink from the recorded ${allowed.size}. ` +
`external production imports of the daemon request/session-state modules may only shrink ` +
`from the recorded ${allowed.size}. ` +
'Use an existing neutral contract; do not move DaemonRequest into contracts to satisfy this gate.',
}));
for (const file of allowed) {
Expand All @@ -223,7 +233,7 @@ function checkDaemonTypesImporters(edges: readonly ResolvedImportEdge[]): Layeri
rule: 'R10 daemon-modularity',
file: 'scripts/layering/daemon-modularity.ts',
line: 1,
message: `${file} no longer imports daemon/types.ts — delete it from externalDaemonTypesImporters in the same change so the dependency cannot return.`,
message: `${file} no longer imports a daemon request/session-state module — delete it from externalDaemonTypesImporters in the same change so the dependency cannot return.`,
});
}
return violations;
Expand Down Expand Up @@ -309,7 +319,7 @@ export function daemonModularitySummary(reference: LayeringRatchets): string {
return (
`R10 holds R7 at the merge-base's ${session.writerOwnedFields} writer-owned fields / ` +
`${session.ownerFileClaims} owner claims, R9 at its ${reference.largestTypeCycle.length} files per zone, ` +
`${DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.length} external daemon/types.ts importers, ` +
`${DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.length} external daemon request/session-state importers, ` +
'and zero forbidden logical-module imports'
);
}
4 changes: 2 additions & 2 deletions scripts/layering/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ test('every SessionState field is classified exactly once', () => {
// R7 by being invisible to the scan, and the rule would silently stop covering part of the
// type it claims to cover.
const fields = sessionStateFields(
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/types.ts'), 'utf8'),
readFileSync(path.resolve(import.meta.dirname, '../../src/daemon/session-state.ts'), 'utf8'),
);
assert.deepEqual(fieldClassificationDrift(fields), []);
assert.equal(
Expand All @@ -425,7 +425,7 @@ test('every SessionState field is classified exactly once', () => {

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

// Unclassified: a field added to SessionState and to neither table. This is the case the
Expand Down
2 changes: 1 addition & 1 deletion scripts/layering/ratchet-reference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const SESSION_TYPES = [

function tree(extra: Record<string, string> = {}) {
return new Map<string, string>([
['src/daemon/types.ts', SESSION_TYPES],
['src/daemon/session-state.ts', SESSION_TYPES],
['src/daemon/session-snapshot.ts', 'session.snapshot = "a"; nextSession.snapshot = "b";'],
['src/daemon/ref-frame.ts', 'session.snapshot = undefined;'],
['src/daemon/handlers/trace-runtime.ts', 'session.trace ??= "t";'],
Expand Down
14 changes: 10 additions & 4 deletions scripts/layering/session-resource-ownership.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ type AstNode = Record<string, unknown>;
export const SESSION_RESOURCE_OWNERSHIP_RULE = 'R68 session-resource-ownership';

const RESOURCE_OWNERS: Readonly<Record<string, ReadonlySet<string>>> = {
appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/types.ts']),
appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/types.ts']),
audioProbe: new Set(['src/daemon/audio-probe-session-resource.ts', 'src/daemon/types.ts']),
perfCapture: new Set(['src/daemon/perf-capture-session-resource.ts', 'src/daemon/types.ts']),
appLog: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']),
appLogFailure: new Set(['src/daemon/app-log-session-resource.ts', 'src/daemon/session-state.ts']),
audioProbe: new Set([
'src/daemon/audio-probe-session-resource.ts',
'src/daemon/session-state.ts',
]),
perfCapture: new Set([
'src/daemon/perf-capture-session-resource.ts',
'src/daemon/session-state.ts',
]),
};

/** Durable session-resource records have one whole-record construction owner per domain. */
Expand Down
32 changes: 25 additions & 7 deletions scripts/layering/session-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,30 @@ export function fieldClassificationDrift(fields: readonly string[]): FieldClassi
return drift.sort((left, right) => left.field.localeCompare(right.field));
}

const SESSION_STATE_DECLARATION = /export type SessionState = \{([\s\S]*?)\n\};/;

/**
* The daemon module that declares `SessionState`, found by the declaration rather than by a
* recorded path. `sessionStateWritePressure` below measures the merge-base tree with the same
* function, and that tree's declaration may still sit where this tree no longer has it — a
* path constant would silently measure such a tree as zero pressure and bank the headroom.
*/
export function sessionStateDeclarationFile(
sources: ReadonlyMap<string, string>,
): string | undefined {
for (const [file, source] of sources) {
if (file.startsWith('src/daemon/') && SESSION_STATE_DECLARATION.test(source)) return file;
}
return undefined;
}

/**
* Field names declared by `SessionState` itself, so the scan cannot be fooled by a daemon
* module with an unrelated local named `session` (a provider session, a runner session).
*/
export function sessionStateFields(typesSource: string): string[] {
const declaration = /export type SessionState = \{([\s\S]*?)\n\};/.exec(typesSource);
if (!declaration) throw new Error('SessionState declaration not found in daemon/types.ts');
const declaration = SESSION_STATE_DECLARATION.exec(typesSource);
if (!declaration) throw new Error('SessionState declaration not found');
return [...declaration[1]!.matchAll(/^ {2}([a-zA-Z][A-Za-z0-9]*)\??:/gm)].map(
(match) => match[1]!,
);
Expand Down Expand Up @@ -294,11 +311,12 @@ export type SessionStateWritePressure = Readonly<{
export function sessionStateWritePressure(
sources: ReadonlyMap<string, string>,
): SessionStateWritePressure {
const types = sources.get('src/daemon/types.ts');
if (!types) return { writerOwnedFields: 0, ownerFileClaims: 0 };
const writes = findSessionStateWrites(sources, sessionStateFields(types)).filter(
(write) => write.field !== '[computed]',
);
const declarationFile = sessionStateDeclarationFile(sources);
if (!declarationFile) return { writerOwnedFields: 0, ownerFileClaims: 0 };
const writes = findSessionStateWrites(
sources,
sessionStateFields(sources.get(declarationFile)!),
).filter((write) => write.field !== '[computed]');
return {
writerOwnedFields: new Set(writes.map((write) => write.field)).size,
ownerFileClaims: new Set(writes.map((write) => `${write.field}\0${write.file}`)).size,
Expand Down
Loading
Loading