Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Read the declaration rather than maintaining a prose copy:

Shared selector parsing and matching belongs in `@agent-device/selectors`; request cancellation
and progress in `@agent-device/capture-kit` (`request-cancel`, `request-progress`); cross-layer
contracts in `src/contracts`; CLI flags in `src/commands/cli-grammar`; cross-surface schema
contracts in `@agent-device/contracts`; CLI flags in `src/commands/cli-grammar`; cross-surface schema
composition in `src/cli-schema`.

The enforced registries are self-declaring. A failing completeness, parity, coverage, timeout,
Expand Down
28 changes: 11 additions & 17 deletions docs/dependency-graph-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ type-only inversions, R7 pins SessionState field ownership, and the shared selec
- Still outside every rule: **dynamic** import direction (0 inversions today, nothing watching),
and anything inside a zone.

## 0. Where the inversions ended up (and why 7 is the floor for now)
## 0. Where the inversions ended up (and why 5 is the floor for now)

61 → 7. The last pass moved four keystones, each of which was pinning a much larger set:
The last pass moved four keystones, each of which was pinning a much larger set:

| Keystone moved to `contracts/` | Unblocked |
|---|---|
Expand Down Expand Up @@ -110,7 +110,7 @@ with `command`/`positionals` `Pick`ed from the wire so they cannot drift from it
resolver already read only those three, in two spellings (the full type and a `Pick` of it); one
narrow name replaced both.

**The remaining 7 are positions, not debt** — each for a mechanical reason, not an appeal to an ADR:
**The remaining 5 are positions, not debt** — each for a mechanical reason, not an appeal to an ADR:

- **4 × `AgentDeviceClient`** (`commands/command-contract.ts`, `commands/command-surface.ts`,
`commands/family/types.ts`, `mcp/command-tools.ts`). The facade cannot move below `commands/`
Expand All @@ -119,20 +119,14 @@ narrow name replaced both.
zone-level type cycle, and breaking it means deciding where the projection registry belongs — a
design call, not a file move. A narrower port does not exist either: 4 files *name* the facade,
but 26 call sites use methods across 13 of its namespaces, so any port would re-declare it.
- **2 × `DaemonCommandDescriptor`** (`core/command-descriptor/derive.ts`, `.../types.ts`). It is
*stated in terms of* the server-private `daemon/types.ts` `DaemonRequest` —
`refFrameEffect?: (req: DaemonRequest) => RefFrameEffect`,
`allowSessionlessDefaultDevice?: (req: DaemonRequest) => boolean` — so it cannot be declared below
the daemon. Having `core/` re-declare a parallel 13-field shape instead would trade one erased
edge for a second source of truth.
- **1 × `DaemonCommandRoute`** (`commands/command-explain.ts`). It is
`keyof typeof DAEMON_ROUTE_HANDLERS` — *computed from* the daemon's handler table, so it cannot
exist below that table. `command-explain.ts` uses it to key an exhaustive
`Record<DaemonCommandRoute, string>` of owner files; a hand-written union in `contracts/` would
drop exactly that exhaustiveness.

All three are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next to the
numbers they explain.
- **1 × `DaemonCommandRoute`** (`commands/command-explain.ts`). The union lives in core so
descriptors can name a route without importing the daemon, and the handler table covers it with
`satisfies Record<DaemonCommandRoute, …>`. `command-explain.ts` still type-imports the re-export
from `daemon-command-registry.ts` to key an exhaustive owner-file map; that remaining inversion
is the commands-zone consumer, not a second source of truth for the union.

All remaining inversions are argued at `TYPE_INVERSION_BASELINE` in `scripts/layering/check.ts`, next
to the numbers they explain.

## 0b. The biggest structural finding is not an inversion

Expand Down
2 changes: 1 addition & 1 deletion packages/ad-replay/src/internal/inspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { resolveReplayEntryIndex, type PendingRecordAndHeal } from './resume.ts'
* #1478 P5 stage C2b: the read-only `.ad` inspection façade. Moved out of
* `session-replay-runtime.ts`'s old `parseReplayScript` (the
* legacy-JSON-payload rejection it guarded) plus the `parseReplayInput`
* composition (`src/compat/replay-input.ts`) it fed into — this is the same
* composition (`parseReplayInput`) it fed into — this is the same
* `parseReplayScriptDetailed` + `readReplayScriptMetadata` pair
* `src/cli/commands/replay.ts` and `session-test-source-discovery.ts` already
* call directly off `@agent-device/ad-script`; nothing beyond the actions,
Expand Down
71 changes: 22 additions & 49 deletions packages/ad-script/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,5 @@
/**
* The `.ad` script codec façade (#1478 P5 scoping dossier, "the codec seam";
* widened by the P5 review pass, "keep genuinely shared recording vocabulary
* in its proper shared owner").
*
* The canonical `.ad` replay script format — read half (parsing a script into
* actions) and write half (formatting actions back into script lines) of one
* artifact, mutually coupled (`script-formatting.ts` calls into the `open`
* action's writer; `script.ts` calls into its parser). Shared by the daemon's
* session-script publication writer, the replay engine's script reader, the
* CLI's `replay export`, and Maestro's failure-label formatting.
*
* Also owns the `# agent-device:target-v1` annotation SERDE (wire type,
* canonical field order, normalization, size caps, payload parsing) and,
* alongside it, the local-identity + ancestry-prefix matching primitives and
* their diagnostic diffs (`target-annotation-identity.ts`) — both record/
* replay-shared `.ad` vocabulary, not engine policy. The companion
* CLASSIFICATION core (`classifyTargetBindingMatch`,
* `target-annotation-classification.ts`, decision 3's replay-time
* verification paths 2-6) moved here too (#1555 review, "complete the
* binding façade instead of documenting deviations"): its only real
* consumers are the daemon's record-time self-check
* (`src/daemon/session-target-evidence.ts`) and replay-time classification
* wrapper (`src/daemon/handlers/session-replay-target-classification.ts`),
* neither reachable through `@agent-device/ad-replay`'s
* `inspectAdReplay`/`runAdReplay`. The annotation SHAPE is not exported here
* either: it lives in `@agent-device/contracts/replay`, which every consumer
* (this package included) imports directly.
*
* Also owns `${VAR}` scope/env/resolution (`vars.ts`): the same script-
* language semantics as `env KEY=VALUE` directive parsing, shared by the
* daemon's replay runtime and the Maestro replay path.
*
* Also owns `resolveDeclaredScriptPlatform` (`open-script.ts`, #1555
* structural-quality review): the platform a script declares before its
* first real `open` (`runtime` actions, then the `open` action's own
* attached hint) — `.ad` script semantics, not engine or daemon policy, and
* needed independently by both `@agent-device/ad-replay`'s plan-digest
* precedence and the daemon's device-selection platform resolution
* (`src/daemon/replay-device-selection.ts`), which is exactly the "shared by
* record/replay AND the daemon" shape this package exists to own.
*/

export {
parseReplayScriptDetailed,
readReplayScriptMetadata,
REPLAY_VAR_KEY_RE,
} from './internal/script.ts';
export type { ParsedReplayScript, ReplayScriptMetadata } from './internal/script.ts';
export { parseReplayScriptDetailed, readReplayScriptMetadata } from './internal/script.ts';
export type { ReplayScriptMetadata } from './internal/script.ts';

export { resolveDeclaredScriptPlatform } from './internal/open-script.ts';

Expand Down Expand Up @@ -101,3 +54,23 @@ export {
readReplayShellEnvSource,
resolveReplayAction,
} from './internal/vars.ts';

export {
isMaestroYamlPath,
maestroBackendRequiredMessage,
resolveReplayFormat,
} from './internal/format.ts';

export {
readRecordedInputVariableName,
recordedInputPlaceholder,
validateRecordedInputVariableName,
} from './internal/recorded-input.ts';

export {
buildAncestryChain,
buildIndexMap,
filterIdentitySet,
} from './internal/target-evidence-tree.ts';

export { parseReplayInput } from './internal/replay-input.ts';
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppError } from '@agent-device/kernel/errors';
import { REPLAY_VAR_KEY_RE } from '@agent-device/ad-script';
import { REPLAY_VAR_KEY_RE } from './script.ts';

const RECORDED_INPUT_PLACEHOLDER_RE = /^\$\{([A-Z_][A-Z0-9_]*)\}$/;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
readReplayScriptMetadata,
type ParsedReplayScript,
type ReplayScriptMetadata,
} from '@agent-device/ad-script';
} from './script.ts';

export type ParsedReplayInput = ParsedReplayScript & {
metadata: ReplayScriptMetadata;
Expand Down
3 changes: 1 addition & 2 deletions packages/ad-script/src/internal/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import {
* underscores, leading with a letter or underscore. Canonical here because
* `env KEY=VALUE` directive parsing is script grammar; the sibling
* `vars.ts` (runtime `${VAR}` resolution) imports it directly, and
* `src/replay/recorded-input.ts` imports it from this package's façade
* rather than duplicating the rule.
* `recorded-input.ts` imports it directly rather than duplicating the rule.
*/
export const REPLAY_VAR_KEY_RE = /^[A-Z_][A-Z0-9_]*$/;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
/**
* ADR 0012 decision 3: the tree-structural evidence primitives shared by the
* record-time writer (`src/daemon/session-target-evidence.ts`), replay-time
* verification (`src/daemon/handlers/session-replay-target-classification.ts`),
* and the command-resolution landmark check `wait` runs inside its polling
* loop (#1349, `src/commands/interaction/runtime/selector-read.ts`). They are
* pure functions over `SnapshotNode` trees; keeping them in the shared
* `replay/` zone lets the commands runtime consume them without importing the
* daemon.
*/

import type { SnapshotNode } from '@agent-device/kernel/snapshot';
import {
matchesAncestryPrefix,
matchesLocalIdentity,
readNodeLocalIdentity,
type LocalIdentity,
} from '@agent-device/ad-script';
} from './target-annotation-identity.ts';
import type { TargetAncestryEntry } from '@agent-device/contracts/replay';

export function buildIndexMap(nodes: readonly SnapshotNode[]): Map<number, SnapshotNode> {
Expand Down
14 changes: 2 additions & 12 deletions packages/contracts/src/ref-frame-effect.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
// ADR 0014 ref-frame effect classification.
//
// A plain string union, and the only part of the daemon command descriptor that zones below the
// daemon actually need: `core/command-descriptor/registry.ts` classifies each command with it while
// composing the descriptor registry. Declaring it beside `DAEMON_ROUTE_HANDLERS` made core reach up
// four ranks for three string literals.
//
// The rest of the descriptor shape deliberately stays in the daemon: `DaemonCommandRoute` is
// `keyof typeof DAEMON_ROUTE_HANDLERS` and `DaemonRefFrameEffect` resolves against the daemon's own
// request type. Moving those down would mean re-declaring the route names in contracts and adding a
// gate to prove the handler map still covers them — more coupling to remove a dependency, which is
// the wrong trade. ADR 0003/0008 own that boundary.
// ADR 0014 ref-frame effect classification. A plain string union so
// `core/command-descriptor/` can classify commands without importing the daemon.

/**
* ADR 0014 session ref-frame lifetime. Declares how a daemon command relates to
Expand Down
4 changes: 4 additions & 0 deletions packages/kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"types": "./src/rect-center.ts",
"default": "./src/rect-center.ts"
},
"./screenshot-geometry": {
"types": "./src/screenshot-geometry.ts",
"default": "./src/screenshot-geometry.ts"
},
"./redaction": {
"types": "./src/redaction.ts",
"default": "./src/redaction.ts"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Rect } from '@agent-device/kernel/snapshot';
import type { Rect } from './snapshot.ts';

export type ImageDimensions = { width: number; height: number };

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 @@ -34,7 +34,7 @@ import type { ConnectVerification } from '../../src/cli/connection/connect-provi
import type { RemoteConnectionState } from '../../src/remote/remote-connection-state.ts';
import { AppError, normalizeError } from '@agent-device/kernel/errors';
import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot';
import { renderSnapshotQualityWarnings } from '../../src/snapshot-quality/warnings.ts';
import { renderSnapshotQualityWarnings } from '../../src/snapshot/snapshot-presentation/quality-warnings.ts';
import { formatSnapshotText, printHumanError } from '../../src/utils/output.ts';

// The production renderer behind each captured sample in
Expand Down
2 changes: 1 addition & 1 deletion scripts/help-conformance-sample-outputs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ hint: The UI kept changing for the whole settle budget (animation, carousel, or

// Recovered snapshot: the private-ax fallback fired but still exposed
// actionable refs. Warning wording is renderSnapshotQualityWarnings
// (src/snapshot-quality/warnings.ts); lines are the structured snapshot
// (src/snapshot/snapshot-presentation/quality-warnings.ts); lines are the structured snapshot
// renderer (src/utils/output.ts formatSnapshotText).
export const PRIVATE_AX_RECOVERY_SAMPLE = {
command: 'agent-device snapshot -i',
Expand Down
29 changes: 13 additions & 16 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import {
} from './platform-package-repository.ts';
import { policyLead, policyViolation, ZONE_POLICIES } from './zone-policy.ts';
import { contractsImplementationAuthorityViolations } from './contracts-implementation-policy.ts';
import { substrateDomainShapeViolations } from './substrate-domain-shape.ts';
import { selectorPipelineOwnershipViolations } from './selector-pipeline-ownership.ts';
import { recordRuntimeRegistryJoinViolations } from './record-runtime-registry-policy.ts';
import { recordRuntimeDaemonMechanicsViolations } from './record-runtime-mechanics-policy.ts';
Expand Down Expand Up @@ -194,7 +195,7 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation

// R6 ratchet: type-only spine inversions, per zone pair. R5 cannot see these (a type-only import
// is free at runtime), but "zone A is declared in terms of zone B" is still a boundary claim, and
// ranking type edges surfaced 61 of them. Down to 7, and every one of the 7 is now a deliberate
// ranking type edges surfaced 61 of them. Down to 5, and every one of the 5 is now a deliberate
// architectural position rather than a misplaced declaration:
//
// commands/mcp -> client (4) `AgentDeviceClient`, used as an opaque handle ("the client this
Expand All @@ -207,20 +208,12 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
// a design call, not a file move. R5 is zero here: nothing imports
// the client at runtime, only its type.
//
// core -> daemon-server (2) `DaemonCommandDescriptor`, which is STATED IN TERMS OF the daemon's
// own server-private `DaemonRequest` (`refFrameEffect`,
// `allowSessionlessDefaultDevice`, `skipSessionlessProviderDevice`
// are all `(req: DaemonRequest) => …`). It therefore cannot be
// declared below the daemon, and having core/ re-declare a parallel
// 13-field shape would trade one erased edge for a second source of
// truth. Zones that only need to CLASSIFY a command take
// `contracts/dispatched-command.ts` instead. ADR 0003/0008.
//
// commands -> daemon-server (1) `DaemonCommandRoute` = `keyof typeof DAEMON_ROUTE_HANDLERS`, so
// it is COMPUTED FROM the daemon's handler table and cannot exist
// below it. `commands/command-explain.ts` uses it to key an
// exhaustive `Record<DaemonCommandRoute, string>` of owner files; a
// hand-written union in contracts/ would drop that exhaustiveness.
// commands -> daemon-server (1) `DaemonCommandRoute` is declared in core so descriptors can
// name a route without importing the daemon. `command-explain.ts`
// still type-imports the re-export from `daemon-command-registry.ts`
// to key an exhaustive `Record<DaemonCommandRoute, string>` of
// owner files; that remaining inversion is the commands-zone
// consumer, not a second source of truth for the union.
//
// See docs/dependency-graph-findings.md §0 for the long form. The counts may only go DOWN. Fixing edges without lowering the number fails too, so the baseline
// cannot quietly stop describing the tree.
Expand All @@ -230,7 +223,6 @@ function checkBackEdges(edges: readonly ResolvedImportEdge[]): LayeringViolation
export const TYPE_INVERSION_BASELINE: Readonly<Record<string, number>> = {
'commands -> client': 3,
'commands -> daemon-server': 1,
'core -> daemon-server': 2,
'mcp -> client': 1,
};

Expand Down Expand Up @@ -544,6 +536,7 @@ export const LAYERING_RULE_IDS = [
'session-resource-ownership',
'application-lifecycle-ownership',
'contracts-implementation-authority',
'substrate-domain-shape',
'selector-pipeline-ownership',
'back-edges',
'type-spine-inversions',
Expand All @@ -570,6 +563,10 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
applicationLifecycleOwnershipViolations(context.sources),
'contracts-implementation-authority': (context) =>
checkContractsImplementationAuthority(context.sources),
'substrate-domain-shape': (context) =>
substrateDomainShapeViolations(
[...context.allTypeScriptSources].map(([path, source]) => ({ path, source })),
),
'selector-pipeline-ownership': (context) =>
selectorPipelineOwnershipViolations(context.edges, workspaceSpecifierTargets(repoRoot)),
'back-edges': (context) => checkBackEdges(context.edges),
Expand Down
17 changes: 17 additions & 0 deletions scripts/layering/contracts-implementation-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,20 @@ test('network traffic vocabulary cannot grow parser implementation inside contra
[],
);
});

test('contracts rejects mutable interaction-outcome lifecycle', () => {
assert.match(
messages('const targets = new WeakMap();', 'packages/contracts/src/interaction-outcome.ts')[0]!,
/src\/core/,
);
});

test('contracts rejects snapshot quality warning rendering', () => {
assert.match(
messages(
'export function renderSnapshotQualityWarnings() { return []; }',
'packages/contracts/src/snapshot-quality-warnings.ts',
)[0]!,
/src\/snapshot\/snapshot-presentation/,
);
});
18 changes: 18 additions & 0 deletions scripts/layering/contracts-implementation-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ export function contractsImplementationAuthorityViolations(
file.source,
parsed.program.body,
);
if (file.path === 'packages/contracts/src/interaction-outcome.ts') {
violations.push(
violation(
file.path,
1,
'contracts may not own mutable interaction-outcome lifecycle; that WeakMap identity map belongs in src/core',
),
);
}
if (file.path === 'packages/contracts/src/snapshot-quality-warnings.ts') {
violations.push(
violation(
file.path,
1,
'contracts may not own snapshot quality warning rendering; that presentation policy belongs in src/snapshot/snapshot-presentation',
),
);
}
if (networkTrafficViolation) violations.push(networkTrafficViolation);
for (const site of moduleSpecifiers(parsed.module, file.source)) {
if (!FORBIDDEN_HOST_MODULES.test(site.spec)) continue;
Expand Down
Loading
Loading