Skip to content

Commit 34bd556

Browse files
committed
working-set: add transfer planning runway
1 parent 59f4299 commit 34bd556

21 files changed

Lines changed: 1271 additions & 11 deletions

ARCHITECTURE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Read-side worldline awareness lives here, not in the reducer:
4949
git-warp now also includes a separate **working-set** substrate family. It is intentionally **not** part of TTD because it creates durable descriptor refs.
5050

5151
- **Domain/core** owns explicit coordinate materialization and working-set descriptors.
52-
- **CLI adapters** expose those capabilities through `working-set create`, `working-set braid`, `working-set list`, `working-set show`, `working-set materialize`, `working-set compare`, and `working-set drop`.
52+
- **CLI adapters** expose those capabilities through `working-set create`, `working-set braid`, `working-set list`, `working-set show`, `working-set materialize`, `working-set compare`, `working-set transfer-plan`, and `working-set drop`.
5353
- **Presenters** render descriptor and materialization summaries without inventing higher-level product semantics.
5454
- **Descriptors pin coordinates; caches do not become truth.**
5555

@@ -59,9 +59,10 @@ The active substrate model is still narrow and substrate-first:
5959
- the target working set keeps its own overlay patch-log ref and can mark that overlay writable or read-only
6060
- zero or more braided read-only overlays can be pinned onto the same base observation
6161
- target-owned braid refs keep those pinned support-overlay heads reachable as durable substrate facts
62-
- read-side helpers such as `getWorkingSetPatches()`, `patchesForWorkingSet()`, `projectStateV5()`, `createStateReaderV5()`, `compareVisibleStateV5()`, `compareWorkingSet()`, `compareCoordinates()`, and working-set-aware conflict analysis operate on the visible `base + braided overlays + active overlay` patch universe
62+
- read-side helpers such as `getWorkingSetPatches()`, `patchesForWorkingSet()`, `projectStateV5()`, `createStateReaderV5()`, `compareVisibleStateV5()`, `compareWorkingSet()`, `compareCoordinates()`, `planWorkingSetTransfer()`, `planCoordinateTransfer()`, and working-set-aware conflict analysis operate on the visible `base + braided overlays + active overlay` patch universe
6363
- debugger payloads can report the selected working-set backing facts directly: base ceiling, overlay head/count/writability, and pinned braid support IDs
6464
- comparison helpers report substrate facts such as visible patch divergence plus node/edge/property deltas instead of higher-layer review or governance meaning
65+
- transfer-planning helpers report substrate-factual candidate settlement ops, including content attach/clear steps, without deciding whether higher layers should execute them
6566
- materialized state is derived/cache only
6667
- no Git worktree assumption leaks into the API
6768

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [14.13.0] — 2026-03-18
11+
12+
### Added
13+
14+
- **Working-set and coordinate transfer-plan API** — Added `WarpGraph.planWorkingSetTransfer()` and `WarpGraph.planCoordinateTransfer()` as deterministic substrate helpers for extracting candidate transfer plans between working sets, base observations, live frontiers, and explicit coordinates without mutating either side.
15+
- **Thin `working-set transfer-plan` CLI surface** — Added `git warp working-set transfer-plan` so operators and higher layers can inspect a substrate-factual settlement runway without turning `debug` into a mutation shell.
16+
17+
### Changed
18+
19+
- **Transfer plans now include attachment deltas explicitly** — candidate plans can carry node/edge content attach and clear operations alongside topology and property updates, so higher layers do not need to reverse-engineer `_content*` substrate keys from materialized state.
20+
- **Working-set docs now describe settlement runway as a substrate concern** — Updated `README.md`, `ARCHITECTURE.md`, `docs/WORKING_SETS.md`, and `docs/TTD.md` so transfer planning is documented as read-only substrate preparation for higher-layer collapse rather than as debugger behavior or application workflow.
21+
1022
## [14.12.0] — 2026-03-18
1123

1224
### Added

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111
<img src="docs/images/hero.gif" alt="git-warp CLI demo" width="600">
1212
</p>
1313

14+
## What's New in v14.13.0
15+
16+
- **Deterministic transfer planning is now a first-class substrate helper**`WarpGraph.planWorkingSetTransfer()` and `WarpGraph.planCoordinateTransfer()` extract a candidate transfer plan from one visible patch universe onto another without mutating either side.
17+
- **Transfer plans stay substrate-factual, including attachment changes** — the returned plan reports only add/remove/set plus explicit content attach/clear operations, deterministic transfer digests, and the resolved source/target coordinates. It does not invent collapse, approval, or governance semantics.
18+
- **The CLI exposes the same settlement runway without turning `debug` into a mutation shell**`git warp working-set transfer-plan` plans a working set transfer into live truth, its pinned base observation, or another working set while keeping TTD read-only.
19+
1420
## What's New in v14.12.0
1521

1622
- **Braid-aware debugger reads now surface their backing working-set context explicitly**`git warp debug timeline`, `debug provenance`, and `debug receipts` now include the resolved working-set overlay head, patch count, writability, base Lamport ceiling, and pinned braid support IDs when `--working-set <id>` is selected.
@@ -101,7 +107,7 @@ If you are new to git-warp, start with the **[Guide](docs/GUIDE.md)**. For deepe
101107
- **[Architecture](ARCHITECTURE.md)**: Deep dive into the hexagonal "Ports and Adapters" design.
102108
- **[CLI Guide](docs/CLI_GUIDE.md)**: Command-by-command reference with examples, flags, and output formats.
103109
- **[Time Travel Debugger](docs/TTD.md)**: Architecture and scope of the thin debugger CLI surface.
104-
- **[Working Sets](docs/WORKING_SETS.md)**: Pinned observation coordinates, comparison helpers, overlay patch-log semantics, and the working-set API/CLI surface.
110+
- **[Working Sets](docs/WORKING_SETS.md)**: Pinned observation coordinates, comparison helpers, transfer planning, overlay patch-log semantics, and the working-set API/CLI surface.
105111
- **Braids**: the canonical term for co-present working-set composition is **braid**. The substrate now supports pinned read-only braid overlays through `braidWorkingSet()` / `git warp working-set braid`, while keeping reducer semantics worldline-blind.
106112
- **[Protocol Specs](docs/specs/)**: Binary formats for Audit Receipts, Content Attachments, and BTRs.
107113
- **[ADR Registry](adr/)**: Architectural Decision Records (e.g., edge-property internal canonicalization).
@@ -627,6 +633,9 @@ git warp working-set braid review-auth --support hold-auth --read-only
627633
# Compare a working set against live truth
628634
git warp working-set compare review-auth --against live --target-id user:alice
629635

636+
# Plan a deterministic transfer from a working set into live truth
637+
git warp working-set transfer-plan review-auth --into live
638+
630639
# Check graph health, status, and GC metrics
631640
git warp check
632641
```
@@ -695,6 +704,9 @@ git warp working-set braid review-auth --support hold-auth --read-only --json
695704

696705
# Compare a working set against another speculative lane
697706
git warp working-set compare review-auth --against working-set:review-auth-b --target-id user:alice --json
707+
708+
# Extract a machine-readable transfer plan without mutating either side
709+
git warp working-set transfer-plan review-auth --into live --json
698710
```
699711
700712
All commands accept `--repo <path>` to target a specific Git repository, `--json` for machine-readable output, and `--view [mode]` for visual output (ascii by default, or `svg:FILE`, `html:FILE`).

bin/cli/commands/working-set.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as dropSubcommand from './working-set/drop.js';
77
import * as listSubcommand from './working-set/list.js';
88
import * as materializeSubcommand from './working-set/materialize.js';
99
import * as showSubcommand from './working-set/show.js';
10+
import * as transferPlanSubcommand from './working-set/transfer-plan.js';
1011

1112
/** @typedef {import('../types.js').CliOptions} CliOptions */
1213
/**
@@ -23,6 +24,7 @@ const WORKING_SET_SUBCOMMANDS = Object.freeze({
2324
[listSubcommand.WORKING_SET_SUBCOMMAND.name]: listSubcommand,
2425
[showSubcommand.WORKING_SET_SUBCOMMAND.name]: showSubcommand,
2526
[compareSubcommand.WORKING_SET_SUBCOMMAND.name]: compareSubcommand,
27+
[transferPlanSubcommand.WORKING_SET_SUBCOMMAND.name]: transferPlanSubcommand,
2628
[materializeSubcommand.WORKING_SET_SUBCOMMAND.name]: materializeSubcommand,
2729
[dropSubcommand.WORKING_SET_SUBCOMMAND.name]: dropSubcommand,
2830
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { z } from 'zod';
2+
3+
import { EXIT_CODES, parseCommandArgs, usageError } from '../../infrastructure.js';
4+
import { openGraph } from '../../shared.js';
5+
6+
/** @typedef {import('../../types.js').CliOptions} CliOptions */
7+
8+
export const WORKING_SET_SUBCOMMAND = Object.freeze({
9+
name: 'transfer-plan',
10+
summary: 'Plan a deterministic transfer from one working set into live, base, or another working set',
11+
});
12+
13+
const TRANSFER_PLAN_OPTIONS = {
14+
into: { type: 'string', default: 'live' },
15+
'lamport-ceiling': { type: 'string' },
16+
'into-lamport-ceiling': { type: 'string' },
17+
};
18+
19+
const transferPlanSchema = z.object({
20+
into: z.string().default('live'),
21+
'lamport-ceiling': z.coerce.number().int().nonnegative().optional(),
22+
'into-lamport-ceiling': z.coerce.number().int().nonnegative().optional(),
23+
}).strict().transform((val, ctx) => {
24+
const rawInto = val.into.trim();
25+
/** @type {'base'|'live'|{ kind: 'working_set', workingSetId: string }} */
26+
let into;
27+
if (rawInto === 'base' || rawInto === 'live') {
28+
into = rawInto;
29+
} else if (rawInto.startsWith('working-set:') && rawInto.length > 'working-set:'.length) {
30+
into = {
31+
kind: /** @type {const} */ ('working_set'),
32+
workingSetId: rawInto.slice('working-set:'.length),
33+
};
34+
} else {
35+
ctx.addIssue({
36+
code: z.ZodIssueCode.custom,
37+
path: ['into'],
38+
message: 'into must be base, live, or working-set:<id>',
39+
});
40+
return z.NEVER;
41+
}
42+
43+
return {
44+
intoRaw: rawInto,
45+
transferOptions: /** @type {{
46+
into?: 'base'|'live'|{ kind: 'working_set', workingSetId: string },
47+
ceiling?: number|null,
48+
intoCeiling?: number|null
49+
}} */ ({
50+
into,
51+
ceiling: val['lamport-ceiling'] ?? null,
52+
intoCeiling: val['into-lamport-ceiling'] ?? null,
53+
}),
54+
};
55+
});
56+
57+
/**
58+
* @param {{options: CliOptions, args: string[]}} params
59+
* @returns {Promise<{payload: unknown, exitCode: number}>}
60+
*/
61+
export async function handleWorkingSetSubcommand({ options, args }) {
62+
const { values, positionals } = parseCommandArgs(args, TRANSFER_PLAN_OPTIONS, transferPlanSchema, {
63+
allowPositionals: true,
64+
});
65+
if (positionals.length !== 1) {
66+
throw usageError(
67+
'Usage: warp-graph working-set transfer-plan <id> [--into live|base|working-set:<id>] [--lamport-ceiling <n>] [--into-lamport-ceiling <n>]',
68+
);
69+
}
70+
71+
const workingSetId = positionals[0];
72+
const { graph, graphName } = await openGraph(options);
73+
const transferPlan = await graph.planWorkingSetTransfer(workingSetId, values.transferOptions);
74+
75+
return {
76+
payload: {
77+
graph: graphName,
78+
workingSetAction: 'transfer-plan',
79+
workingSetId,
80+
into: values.intoRaw,
81+
transferPlan,
82+
},
83+
exitCode: EXIT_CODES.OK,
84+
};
85+
}

bin/cli/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
* @property {(workingSetId: string, options?: {ceiling?: number|null}) => Promise<Array<{patch: import('../../src/domain/types/WarpTypesV2.js').PatchV2, sha: string}>>} getWorkingSetPatches
4141
* @property {(workingSetId: string, entityId: string, options?: {ceiling?: number|null}) => Promise<string[]>} patchesForWorkingSet
4242
* @property {(workingSetId: string, options?: {against?: 'base'|'live'|{kind: 'working_set', workingSetId: string}, ceiling?: number|null, againstCeiling?: number|null, targetId?: string|null}) => Promise<import('../../index.js').CoordinateComparisonV1>} compareWorkingSet
43+
* @property {(workingSetId: string, options?: {into?: 'base'|'live'|{kind: 'working_set', workingSetId: string}, ceiling?: number|null, intoCeiling?: number|null}) => Promise<import('../../index.js').CoordinateTransferPlanV1>} planWorkingSetTransfer
4344
* @property {(options: {left: import('../../index.js').CoordinateComparisonSelectorV1, right: import('../../index.js').CoordinateComparisonSelectorV1, targetId?: string|null}) => Promise<import('../../index.js').CoordinateComparisonV1>} compareCoordinates
45+
* @property {(options: {source: import('../../index.js').CoordinateTransferPlanSelectorV1, target: import('../../index.js').CoordinateTransferPlanSelectorV1}) => Promise<import('../../index.js').CoordinateTransferPlanV1>} planCoordinateTransfer
4446
* @property {() => Promise<{ticks: number[], maxTick: number, perWriter: Map<string, WriterTickInfo>}>} discoverTicks
4547
* @property {(sha: string) => Promise<import('../../src/domain/types/WarpTypesV2.js').PatchV2>} loadPatchBySha
4648
* @property {(cache: import('../../src/ports/SeekCachePort.js').default) => void} setSeekCache

bin/presenters/text.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,15 @@ import { formatStructuralDiff } from '../../src/visualization/renderers/ascii/se
181181
* against: string,
182182
* comparison: import('../../index.js').CoordinateComparisonV1
183183
* }} WorkingSetComparePayload
184+
* @typedef {{
185+
* graph: string,
186+
* workingSetAction: 'transfer-plan',
187+
* workingSetId: string,
188+
* into: string,
189+
* transferPlan: import('../../index.js').CoordinateTransferPlanV1
190+
* }} WorkingSetTransferPlanPayload
184191
* @typedef {DebugConflictsPayload | DebugCoordinatePayload | DebugProvenancePayload | DebugReceiptsPayload | DebugTimelinePayload} DebugPayload
185-
* @typedef {WorkingSetShowPayload | WorkingSetListPayload | WorkingSetDropPayload | WorkingSetMaterializePayload | WorkingSetComparePayload} WorkingSetPayload
192+
* @typedef {WorkingSetShowPayload | WorkingSetListPayload | WorkingSetDropPayload | WorkingSetMaterializePayload | WorkingSetComparePayload | WorkingSetTransferPlanPayload} WorkingSetPayload
186193
*/
187194

188195
// ── ANSI helpers ─────────────────────────────────────────────────────────────
@@ -1215,6 +1222,27 @@ export function renderWorkingSet(payload) {
12151222
return `${lines.join('\n')}\n`;
12161223
}
12171224

1225+
if (payload.workingSetAction === 'transfer-plan') {
1226+
const { transferPlan } = payload;
1227+
const { summary } = transferPlan;
1228+
const lines = [
1229+
`Graph: ${payload.graph}`,
1230+
'Working Set Action: transfer-plan',
1231+
`Working Set: ${payload.workingSetId}`,
1232+
`Into: ${payload.into}`,
1233+
`Transfer Digest: ${transferPlan.transferDigest}`,
1234+
`Comparison Digest: ${transferPlan.comparisonDigest}`,
1235+
`Changed: ${transferPlan.changed ? 'yes' : 'no'}`,
1236+
`Source: ${transferPlan.source.resolved.coordinateKind} patches=${transferPlan.source.resolved.summary.patchCount} nodes=${transferPlan.source.resolved.summary.nodeCount} edges=${transferPlan.source.resolved.summary.edgeCount}`,
1237+
`Target: ${transferPlan.target.resolved.coordinateKind} patches=${transferPlan.target.resolved.summary.patchCount} nodes=${transferPlan.target.resolved.summary.nodeCount} edges=${transferPlan.target.resolved.summary.edgeCount}`,
1238+
`Plan Summary: ops=${summary.opCount} addNodes=${summary.addNodeCount} removeNodes=${summary.removeNodeCount} addEdges=${summary.addEdgeCount} removeEdges=${summary.removeEdgeCount}`,
1239+
`Property Ops: nodeSet=${summary.setNodePropertyCount} nodeClear=${summary.clearNodePropertyCount} edgeSet=${summary.setEdgePropertyCount} edgeClear=${summary.clearEdgePropertyCount}`,
1240+
`Content Ops: nodeAttach=${summary.attachNodeContentCount} nodeClear=${summary.clearNodeContentCount} edgeAttach=${summary.attachEdgeContentCount} edgeClear=${summary.clearEdgeContentCount}`,
1241+
];
1242+
1243+
return `${lines.join('\n')}\n`;
1244+
}
1245+
12181246
const lines = [
12191247
`Graph: ${payload.graph}`,
12201248
`Working Set Action: ${payload.workingSetAction}`,

contracts/type-surface.m8.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,15 @@
13901390
"CoordinateComparisonV1": {
13911391
"kind": "interface"
13921392
},
1393+
"CoordinateTransferPlanSelectorV1": {
1394+
"kind": "type"
1395+
},
1396+
"CoordinateTransferPlanSideV1": {
1397+
"kind": "type"
1398+
},
1399+
"CoordinateTransferPlanV1": {
1400+
"kind": "interface"
1401+
},
13931402
"ComposeWormholesOptions": {
13941403
"kind": "interface"
13951404
},
@@ -1666,6 +1675,12 @@
16661675
"VisibleStateSummaryV5": {
16671676
"kind": "interface"
16681677
},
1678+
"VisibleStateTransferOperationV1": {
1679+
"kind": "type"
1680+
},
1681+
"VisibleStateTransferPlanSummaryV1": {
1682+
"kind": "interface"
1683+
},
16691684
"WarpGraphStatus": {
16701685
"kind": "interface"
16711686
},

docs/TTD.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ Separate but adjacent:
9393
Pins read-only braid support overlays onto a target working set without changing the TTD read-only contract.
9494
- `git warp working-set compare`
9595
Compares durable coordinates and visible patch universes. It stays outside `debug` because it is a coordinate-comparison surface, not a single-coordinate debugger topic.
96+
- `git warp working-set transfer-plan`
97+
Extracts a deterministic candidate transfer between durable coordinates. It stays outside `debug` because settlement-runway planning is adjacent to, but distinct from, single-coordinate time-travel inspection.
9698

9799
## Hexagonal Boundary
98100

@@ -116,13 +118,15 @@ TTD is also deliberately separate from working-set management:
116118

117119
- debug commands inspect substrate facts
118120
- working-set commands pin durable coordinates and compare them
121+
- `working-set transfer-plan` plans substrate-factual transfer without deciding application-level settlement
119122
- higher layers may combine both, but git-warp keeps the boundary explicit
120123
- higher-layer library code that needs the same visible truth can combine
121124
`materializeWorkingSet()` with `projectStateV5()` or `createStateReaderV5()`
122125
without turning git-warp into an application query framework
123126
- coordinate comparison helpers such as `compareWorkingSet()`,
124-
`compareCoordinates()`, and `compareVisibleStateV5()` stay substrate-factual
125-
and do not collapse into application-level decision semantics
127+
`compareCoordinates()`, `compareVisibleStateV5()`, `planWorkingSetTransfer()`,
128+
and `planCoordinateTransfer()` stay substrate-factual and do not collapse
129+
into application-level decision semantics
126130

127131
## Read-Only Contract
128132

@@ -162,6 +166,7 @@ This keeps TTD aligned with the current git-warp substrate model:
162166
- `debug timeline`, `debug conflicts`, `debug provenance`, and `debug receipts` can inspect a pinned working set, including any pinned braid support overlays, without teaching the reducer about worldlines
163167
- those braid-aware debug topics can also report which pinned overlay/braid context backed the read, so receipts and provenance stay auditable instead of implicit
164168
- `working-set compare` handles deterministic coordinate/working-set divergence reads outside the debugger family
169+
- `working-set transfer-plan` handles deterministic candidate-transfer extraction outside the debugger family
165170
- `working-set braid` changes descriptor visibility, not debugger semantics
166171
- explicit working-set descriptors pin positions without mutating the debugger family
167172
- higher layers may later project richer worldline semantics on top

0 commit comments

Comments
 (0)