@@ -16,17 +16,18 @@ should treat restored data, chunker output, codec output, and keys as
1616
17171 . [ ContentAddressableStore] ( #contentaddressablestore )
18182 . [ Application Storage] ( #application-storage )
19- 3 . [ Root Sets] ( #root-sets )
20- 4 . [ Cache Sets] ( #cache-sets )
21- 5 . [ Expiring Sets] ( #expiring-sets )
22- 6 . [ Repository Diagnostics] ( #repository-diagnostics )
23- 7 . [ Vault] ( #vault )
24- 8 . [ CasService] ( #casservice )
25- 9 . [ Events] ( #events )
26- 10 . [ Value Objects] ( #value-objects )
27- 11 . [ Ports] ( #ports )
28- 12 . [ Codecs] ( #codecs )
29- 13 . [ Error Codes] ( #error-codes )
19+ 3 . [ Scoped Staging Workspaces] ( #scoped-staging-workspaces )
20+ 4 . [ Root Sets] ( #root-sets )
21+ 5 . [ Cache Sets] ( #cache-sets )
22+ 6 . [ Expiring Sets] ( #expiring-sets )
23+ 7 . [ Repository Diagnostics] ( #repository-diagnostics )
24+ 8 . [ Vault] ( #vault )
25+ 9 . [ CasService] ( #casservice )
26+ 10 . [ Events] ( #events )
27+ 11 . [ Value Objects] ( #value-objects )
28+ 12 . [ Ports] ( #ports )
29+ 13 . [ Codecs] ( #codecs )
30+ 14 . [ Error Codes] ( #error-codes )
3031
3132## ContentAddressableStore
3233
@@ -1239,6 +1240,137 @@ generation later.
12391240` toJSON() ` serializes ` handle ` to its canonical token and copies the root
12401241evidence fields.
12411242
1243+ ## Scoped Staging Workspaces
1244+
1245+ Scoped staging workspaces retain intermediate application handles while a
1246+ caller builds a larger asset or bundle. They are the high-level temporary
1247+ reachability API. Use them instead of managing Git refs, raw CAS objects, or a
1248+ general CacheSet as a build scratchpad.
1249+
1250+ ``` javascript
1251+ const workspace = await cas .workspaces .open ({
1252+ namespace: ' git-warp/materializations' ,
1253+ ttlMs: 2 * 60 * 60 * 1000 ,
1254+ });
1255+
1256+ try {
1257+ const first = await workspace .pages .put ({ source: firstShard });
1258+ const second = await workspace .pages .put ({ source: secondShard });
1259+ const bundle = await workspace .bundles .putOrdered ({
1260+ members: [
1261+ [' shards/first.cbor' , first .handle ],
1262+ [' shards/second.cbor' , second .handle ],
1263+ ],
1264+ });
1265+
1266+ await workspace .checkpoint ({ handles: [bundle .handle ] });
1267+ return await workspace .promoteToCache ({
1268+ cache,
1269+ key: materializationKey,
1270+ handle: bundle .handle ,
1271+ options: { retention: ' evictable' },
1272+ });
1273+ } finally {
1274+ await workspace .release ();
1275+ }
1276+ ```
1277+
1278+ ` cas.workspaces.open({ namespace, ttlMs }) ` creates an in-memory workspace. It
1279+ does not create a Git ref until the first successful stage or checkpoint. The
1280+ default TTL is two hours; the maximum is seven days. ` ttlMs ` must be a positive
1281+ safe integer.
1282+
1283+ The workspace mirrors only application-storage writes:
1284+
1285+ ``` javascript
1286+ await workspace .assets .put (options);
1287+ await workspace .assets .adopt (options);
1288+ await workspace .pages .put (options);
1289+ await workspace .bundles .put (options);
1290+ await workspace .bundles .putOrdered (options);
1291+ ```
1292+
1293+ Each method returns only after a direct workspace generation reaches the
1294+ returned typed handle. The result is otherwise the ordinary staged result plus
1295+ a workspace ` RetentionWitness ` . Calls on one workspace serialize their ref
1296+ mutations so concurrent staging cannot silently lose an accumulated root.
1297+
1298+ This guarantee starts when the method returns. Like all Git object composition,
1299+ the object-write-to-ref-update interval still relies on Git's ordinary
1300+ unreachable-object grace period. Running immediate-expiry prune concurrently
1301+ inside that interval is unsupported.
1302+
1303+ ### Checkpoint, renew, promote, and release
1304+
1305+ ` checkpoint({ handles }) ` replaces the active roots with the unique supplied
1306+ handles. Use it after an aggregate bundle transitively reaches its components.
1307+ An empty checkpoint removes active targets and installs a descriptor-only lease
1308+ generation, leaving the workspace available for later staging.
1309+
1310+ ` renew() ` preserves the current target set and advances its lease descriptor.
1311+ Successful staging, checkpoint, and promotion preparation also renew the
1312+ workspace.
1313+
1314+ ` promoteToCache({ cache, key, handle, options }) ` and
1315+ ` promoteToPublication({ handle, commit, ref }) ` establish destination
1316+ retention before releasing the workspace. The promoted handle must belong to
1317+ the active generation. The destination must return an anchored witness for the
1318+ exact handle, ref, and generation. Rejection, missing evidence, or destination
1319+ failure leaves the workspace intact and reports
1320+ ` WORKSPACE_PROMOTION_NOT_RETAINED ` where applicable. If destination retention
1321+ succeeds but workspace cleanup conflicts, the operation fails with
1322+ ` WORKSPACE_PROMOTION_CLEANUP_PENDING ` ; its metadata distinguishes retained
1323+ destination state from pending temporary cleanup.
1324+
1325+ ` release() ` is idempotent for one workspace object and deletes only its exact
1326+ observed direct-ref generation. A conflicting generation or symbolic ref fails
1327+ closed. Calling any staging, checkpoint, renewal, or promotion method after a
1328+ successful release fails with ` WORKSPACE_RELEASED ` .
1329+
1330+ ### Inspect and sweep
1331+
1332+ Workspace expiry is operational posture, not automatic revocation. An expired
1333+ workspace remains Git-reachable until an explicit checked sweep removes it.
1334+
1335+ ``` javascript
1336+ const inspection = await cas .workspaces .inspect ({
1337+ namespace: ' git-warp/materializations' ,
1338+ limit: 100 ,
1339+ });
1340+
1341+ let cursor = null ;
1342+ do {
1343+ const cleanup = await cas .workspaces .sweep ({
1344+ namespace: ' git-warp/materializations' ,
1345+ limit: 100 ,
1346+ cursor,
1347+ });
1348+ cursor = cleanup .nextCursor ;
1349+ } while (cursor !== null );
1350+ ```
1351+
1352+ Inspection is namespace-scoped and bounded. Each record reports identity,
1353+ generation, direct-root count, creation time, age, expiry, posture, and two
1354+ different byte measures:
1355+
1356+ - ` logicalBytes ` sums the validated semantic content size reported by each
1357+ retained typed handle. Overlapping handles can describe overlapping content.
1358+ - ` rootObjectBytes ` sums each unique direct Git root object's loose logical
1359+ size. It excludes transitive support, deduplication attribution, pack
1360+ compression, and filesystem overhead.
1361+
1362+ Inspection validates the canonical target name, typed handle, OID, Git object
1363+ type, and complete application handle graph. Invalid persisted state produces
1364+ ` posture: 'invalid' ` with structured issue evidence instead of a guessed byte
1365+ count.
1366+
1367+ ` sweep() ` deletes only records observed as expired, direct, and still at the
1368+ same generation. Inspection and sweep return an opaque ` nextCursor ` whenever
1369+ ` truncated ` is true; pass that cursor to the next call so active or invalid
1370+ records cannot starve later expired workspaces. The sweep result reports
1371+ inspected, changed, conflicted, missing, and truncated counts. Sweep never
1372+ removes active or invalid records.
1373+
12421374## Root Sets
12431375
12441376Root sets retain a mutable current set of Git blobs or trees. They are intended
@@ -1786,14 +1918,15 @@ const report = await cas.diagnostics.doctor({
17861918console .log (report .repository .objects );
17871919console .log (report .usage .acquisitions );
17881920console .log (report .usage .caches );
1921+ console .log (report .usage .workspaces );
17891922console .log (report .limitations );
17901923```
17911924
17921925Pass either ` gracePeriodMs ` or an exact canonical ` expiresBefore ` UTC timestamp,
17931926not both. The default grace period is 14 days. ` maxCollectionsPerKind ` bounds
1794- detailed cache-acquisition, CacheSet, RootSet, and ExpiringSet rows from 1
1795- through 1000; its default is 100. Every managed collection or acquisition is
1796- still inspected sequentially and included in ` totals ` . Coverage reports
1927+ detailed cache-acquisition, CacheSet, RootSet, ExpiringSet, and workspace rows
1928+ from 1 through 1000; its default is 100. Every managed collection or
1929+ acquisition is still inspected sequentially and included in ` totals ` . Coverage reports
17971930` observed ` , ` inspected ` , ` detailed ` , and ` complete ` , so detail truncation is
17981931visible instead of silently dropping managed refs or undercounting repository
17991932usage.
@@ -1831,9 +1964,12 @@ retention ref remains healthy, `ageMs` is `null`, and the entry reports
18311964` maxAgeMs ` is ` null ` when any active acquisition age is incomparable. Cache
18321965summaries report entry count, deterministic logical bytes, age,
18331966expiry, capacity policy, and pinned/evictable counts. RootSet policy counts and
1834- Vault entry counts are reported independently from reachability. A privacy-mode
1835- vault remains healthy but reports ` entryCount: null ` because repository doctor
1836- does not request or retain vault key material.
1967+ Vault entry counts are reported independently from reachability. Workspace
1968+ summaries report active and expired counts, validated logical bytes, and unique
1969+ direct-root object bytes; detailed rows carry creation age, exact expiry, and
1970+ invalid-state evidence. Neither workspace byte field is physical residency. A
1971+ privacy-mode vault remains healthy but reports ` entryCount: null ` because
1972+ repository doctor does not request or retain vault key material.
18371973
18381974Git's ` for-each-ref ` does not enumerate dangling symbolic refs. Repository
18391975doctor therefore reports symbolic acquisition refs that Git returns, but does
@@ -3195,6 +3331,16 @@ new CasError({ message, code, meta, documentationUrl });
31953331| `ROOT_SET_METADATA_INVALID` | `.rootset.json` is malformed, non-canonical, or belongs to another ref | `read()`, `list()`, `doctor()` |
31963332| `ROOT_SET_TREE_INVALID` | Metadata and the Git tree's actual reachability edges disagree | `read()`, `list()`, `doctor()` |
31973333| `ROOT_SET_REF_UPDATE_FAILED` | Root-set ref update failed for a non-conflict reason | Root-set mutations and repair |
3334+ | `WORKSPACE_REF_INVALID` | Workspace ref namespace, identity, epoch, or canonical encoding is invalid | Workspace creation, inspection, and cleanup |
3335+ | `WORKSPACE_DESCRIPTOR_INVALID` | Workspace lease descriptor is malformed, non-canonical, inconsistent, or outside bounds | Workspace renewal, inspection, and cleanup |
3336+ | `WORKSPACE_STATE_INVALID` | Retained target names, handles, OIDs, types, counts, or byte evidence disagree | Workspace staging, checkpoint, inspection, and doctor |
3337+ | `WORKSPACE_TTL_INVALID` | Workspace TTL or computed expiry is outside supported bounds | `workspaces.open()`, staging, checkpoint, and renewal |
3338+ | `WORKSPACE_CONFLICT` | Workspace generation changed during a checked operation | Workspace inspection and sweep |
3339+ | `WORKSPACE_RELEASED` | A mutating operation targeted an already released workspace | Workspace staging, checkpoint, renewal, and promotion |
3340+ | `WORKSPACE_RETENTION_FAILED` | A low-level stage succeeded but its handle could not be proven in a workspace generation; metadata carries the staged receipt | Workspace asset, page, and bundle staging |
3341+ | `WORKSPACE_HANDLE_NOT_RETAINED` | Promotion requested a handle absent from the active workspace generation | `promoteToCache()`, `promoteToPublication()` |
3342+ | `WORKSPACE_PROMOTION_NOT_RETAINED` | Destination did not return exact anchored retention evidence, so the workspace remains active | `promoteToCache()`, `promoteToPublication()` |
3343+ | `WORKSPACE_PROMOTION_CLEANUP_PENDING` | Destination retention succeeded but checked workspace release failed | `promoteToCache()`, `promoteToPublication()` |
31983344| `REPOSITORY_INSPECTION_INVALID` | Repository doctor options, Git output, dependencies, or safe-integer totals are invalid | `cas.diagnostics.doctor()`, repository inspection adapter |
31993345| `VAULT_ENTRY_NOT_FOUND` | Slug does not exist in vault | `removeFromVault()`, `resolveVaultEntry()` |
32003346| `VAULT_ENTRY_EXISTS` | Slug already exists (use `force` to overwrite) | `addToVault()` |
0 commit comments