Skip to content

Commit ff1becc

Browse files
committed
refactor(daemon): add the tagged script-publication aggregate
First step of P4a. Eight co-resident optional SessionState fields encoded two unrelated lifecycles plus their shared output target, with nothing in the shape saying the two are disjoint — so readers re-derived that from field combinations and writers had to remember which siblings to clear. The aggregate makes both invariants structural rather than remembered: - a session publishes nothing, authors ordinarily, or is under repair, never two at once; - force lives inside the target, so retargeting replaces the authorization along with the path. There is no separate flag left behind to forget, which is what makes silently overwriting an unauthorized file unreachable (#1258). Values and pure transitions only; no authority. The capability that performs close sequencing and atomic publication is separate. No writers migrated yet — that lands in the same PR, per the brief's requirement that the aggregate and both projections arrive atomically with no shadow repair state. Refs #1478 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
1 parent 9922cd1 commit ff1becc

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* The tagged script-publication aggregate (#1478 P4a).
3+
*
4+
* Eight co-resident optional fields on `SessionState` previously encoded two unrelated
5+
* lifecycles plus their shared output target:
6+
*
7+
* - `scriptRecordingState` — the ADR 0016 ordinary open-to-destination authoring lifecycle;
8+
* - `saveScriptBoundary`/`saveScriptComplete`/`saveScriptCommitted`/`repairPlatformCloseReceipt`/
9+
* `repairSourcePath` — the ADR 0012 decision 6 repair transaction;
10+
* - `saveScriptPath`/`saveScriptForce` — the output target, shared by both.
11+
*
12+
* Nothing in the shape said the two lifecycles are disjoint, so every reader re-derived that
13+
* from field combinations ("boundary set and complete unset means ARMED"), and every writer
14+
* had to remember which siblings to clear. This aggregate makes the disjointness structural:
15+
* a session is publishing nothing, authoring ordinarily, or under repair — never two at once.
16+
*
17+
* Force lives INSIDE the target rather than beside it, which is the other invariant worth
18+
* making structural. `--force` is a per-target authorization, not a session-wide standing
19+
* grant: re-arming a different `--save-script=<other>` without a live `--force` must clear it,
20+
* so a later retarget can never silently overwrite a file the caller never opted into (#1258).
21+
* Because authorization is a field of the target, replacing the target replaces its
22+
* authorization — retargeting cannot forget to clear force, because there is no separate flag
23+
* left behind to forget.
24+
*
25+
* This module is daemon-private and holds no authority: it is values and pure transitions.
26+
* The capability that performs close sequencing and atomic publication is separate, and no
27+
* engine can reach either.
28+
*/
29+
30+
/**
31+
* Where a script publishes, and whether the caller authorized overwriting it.
32+
*
33+
* `force` is captured at the moment `--save-script` was armed and persisted, so a later write
34+
* that does not repeat the flag — a bare `close` finishing a session opened with
35+
* `open --save-script --force`, a `--from` continuation leg, or an unattended auto-commit
36+
* teardown with no live request at all — still honors the overwrite the caller opted into.
37+
*/
38+
export type SessionScriptTarget = Readonly<{
39+
path: string;
40+
force: boolean;
41+
}>;
42+
43+
/**
44+
* ADR 0012 decision 6 repair status.
45+
*
46+
* ARMED -> COMPLETE -> CLOSE-SUCCEEDED -> COMMITTED is the success path; ABORTED is terminal
47+
* from any earlier state. Commit is gated on COMPLETE, never on a bare `close`: a
48+
* `close`/`close --save-script` issued after a divergence but before the plan reaches its
49+
* final executable step discards the prefix instead of committing it (C2/C4).
50+
*/
51+
export type SessionScriptRepairStatus =
52+
| 'armed'
53+
| 'complete'
54+
| 'close-succeeded'
55+
| 'committed'
56+
| 'aborted';
57+
58+
export type SessionScriptPublicationState =
59+
| Readonly<{ kind: 'none' }>
60+
/** ADR 0016 ordinary open-to-destination authoring. Disjoint from repair by construction. */
61+
| Readonly<{
62+
kind: 'authoring';
63+
status: 'armed' | 'aborted' | 'published';
64+
target: SessionScriptTarget;
65+
}>
66+
| Readonly<{
67+
kind: 'repair';
68+
status: SessionScriptRepairStatus;
69+
target: SessionScriptTarget;
70+
/**
71+
* `session.actions.length` when `replay --save-script` armed this session. The healed
72+
* `.ad` serializes only actions from this index onward, so a reused session's earlier,
73+
* unrelated actions never leak into the healed script (R6).
74+
*/
75+
boundary: number;
76+
/**
77+
* The original replay input path, stashed so an idle-reap tombstone can hand the agent
78+
* an actionable `replay <path> --save-script` re-run instead of a bare SESSION_NOT_FOUND
79+
* (C5a).
80+
*/
81+
sourcePath?: string;
82+
/**
83+
* Identity of the platform-close operation that succeeded, retained across publication
84+
* retries so the same operation does not re-dispatch a close. Set only at
85+
* `close-succeeded` and later; a different operation identity dispatches a fresh close.
86+
*/
87+
closeReceipt?: string;
88+
}>;
89+
90+
export const NO_SCRIPT_PUBLICATION: SessionScriptPublicationState = { kind: 'none' };
91+
92+
/**
93+
* Applies an arming request to the current state, honoring per-target force authorization.
94+
*
95+
* Re-arming the SAME target keeps an existing authorization: the caller already opted in and a
96+
* bare re-arm is not a withdrawal. Arming a DIFFERENT target takes only what this request
97+
* grants, which is what makes a silent overwrite of an unauthorized file unreachable.
98+
*/
99+
export function resolveScriptTarget(
100+
previous: SessionScriptTarget | undefined,
101+
requested: Readonly<{ path: string; force: boolean }>,
102+
): SessionScriptTarget {
103+
const retainsAuthorization = previous?.path === requested.path && previous.force;
104+
return { path: requested.path, force: requested.force || retainsAuthorization };
105+
}
106+
107+
/** The target a state publishes to, or `undefined` when it publishes nothing. */
108+
export function scriptPublicationTarget(
109+
state: SessionScriptPublicationState,
110+
): SessionScriptTarget | undefined {
111+
return state.kind === 'none' ? undefined : state.target;
112+
}
113+
114+
/**
115+
* Whether a repair transaction has reached its final executable step with no outstanding
116+
* divergence, which is what gates commit (C2).
117+
*/
118+
export function isRepairCommittable(state: SessionScriptPublicationState): boolean {
119+
return (
120+
state.kind === 'repair' && (state.status === 'complete' || state.status === 'close-succeeded')
121+
);
122+
}
123+
124+
/** A committed publication is idempotent: a second write no-ops rather than republishing. */
125+
export function isScriptPublished(state: SessionScriptPublicationState): boolean {
126+
if (state.kind === 'repair') return state.status === 'committed';
127+
return state.kind === 'authoring' && state.status === 'published';
128+
}

0 commit comments

Comments
 (0)