Skip to content

Commit bbc9a21

Browse files
authored
Merge pull request #4 from git-stunts/compass
feat: M4 Compass — lifecycle management
2 parents 80bb5c2 + ad2dbcb commit bbc9a21

7 files changed

Lines changed: 1568 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [Unreleased] — M4 Compass
9+
10+
### Added
11+
- `CasService.readManifest({ treeOid })` — reads a Git tree, locates and decodes the manifest, returns a validated `Manifest` value object.
12+
- `CasService.deleteAsset({ treeOid })` — returns logical deletion metadata (`{ slug, chunksOrphaned }`) without performing destructive Git operations.
13+
- `CasService.findOrphanedChunks({ treeOids })` — aggregates referenced chunk blob OIDs across multiple assets, returning `{ referenced: Set<string>, total: number }`.
14+
- Facade pass-throughs for `readManifest`, `deleteAsset`, and `findOrphanedChunks` on `ContentAddressableStore`.
15+
- New error codes: `MANIFEST_NOT_FOUND`, `GIT_ERROR`.
16+
- 42 new unit tests across three new test suites.
917

1018
## [1.3.0] — M3 Launchpad (2026-02-06)
1119

ROADMAP.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ Return and throw semantics for every public method (current and planned).
128128

129129
| Version | Milestone | Codename | Theme |
130130
|--------:|-----------|----------|-------|
131-
| v1.1.0 | M1 | Bedrock | Foundation hardening |
132-
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI |
133-
| v1.3.0 | M3 | Launchpad| CI/CD pipeline |
134-
| v1.4.0 | M4 | Compass | Lifecycle management |
131+
| v1.1.0 | M1 | Bedrock | Foundation hardening ||
132+
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI ||
133+
| v1.3.0 | M3 | Launchpad| CI/CD pipeline ||
134+
| v1.4.0 | M4 | Compass | Lifecycle management ||
135135
| v1.5.0 | M5 | Sonar | Observability |
136136
| v1.6.0 | M6 | Cartographer | Documentation |
137137
| v2.0.0 | M7 | Horizon | Advanced features |
@@ -178,7 +178,7 @@ M3 Launchpad (v1.3.0) M4 Compass (v1.4.0)
178178

179179
---
180180

181-
# M1 — Bedrock (v1.1.0)
181+
# M1 — Bedrock (v1.1.0)
182182
**Theme:** Close compliance gaps, harden validation, expand test coverage. No new features.
183183

184184
---
@@ -550,7 +550,7 @@ As a maintainer, I want error conditions covered by tests so regressions in vali
550550

551551
---
552552

553-
# M2 — Boomerang (v1.2.0)
553+
# M2 — Boomerang (v1.2.0)
554554
**Theme:** Complete store→retrieve round trip + CLI.
555555

556556
---
@@ -903,7 +903,7 @@ As a developer, I want `git cas restore <tree-oid> --out <path>` so I can retrie
903903

904904
---
905905

906-
# M3 — Launchpad (v1.3.0)
906+
# M3 — Launchpad (v1.3.0)
907907
**Theme:** Automated quality gates and release process.
908908

909909
---
@@ -1014,12 +1014,12 @@ As a maintainer, I want releases published automatically on version tags so publ
10141014

10151015
---
10161016

1017-
# M4 — Compass (v1.4.0)
1017+
# M4 — Compass (v1.4.0)
10181018
**Theme:** Read manifests from Git, manage stored assets, analyze storage.
10191019

10201020
---
10211021

1022-
## Task 4.1: Implement readManifest() on CasService
1022+
## Task 4.1: Implement readManifest() on CasService
10231023

10241024
**User Story**
10251025
As a developer, I want to reconstruct a Manifest from a Git tree OID so I can inspect and restore assets without holding manifests in memory.
@@ -1073,7 +1073,7 @@ As a developer, I want to reconstruct a Manifest from a Git tree OID so I can in
10731073

10741074
---
10751075

1076-
## Task 4.2: Implement deleteAsset() (logical unlink info)
1076+
## Task 4.2: Implement deleteAsset() (logical unlink info)
10771077

10781078
**User Story**
10791079
As a developer, I want to "delete" an asset logically so I can manage lifecycle even though Git GC handles physical deletion.
@@ -1124,7 +1124,7 @@ As a developer, I want to "delete" an asset logically so I can manage lifecycle
11241124

11251125
---
11261126

1127-
## Task 4.3: Implement orphaned chunk analysis
1127+
## Task 4.3: Implement orphaned chunk analysis
11281128

11291129
**User Story**
11301130
As an operator, I want to identify referenced chunks across many assets so I can assess storage waste.

index.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,28 @@ export default class ContentAddressableStore {
177177
const service = await this.#getService();
178178
return await service.verifyIntegrity(manifest);
179179
}
180+
181+
/**
182+
* Reads a manifest from a Git tree OID.
183+
*/
184+
async readManifest(options) {
185+
const service = await this.#getService();
186+
return await service.readManifest(options);
187+
}
188+
189+
/**
190+
* Returns deletion metadata for an asset stored in a Git tree.
191+
*/
192+
async deleteAsset(options) {
193+
const service = await this.#getService();
194+
return await service.deleteAsset(options);
195+
}
196+
197+
/**
198+
* Aggregates referenced chunk blob OIDs across multiple stored assets.
199+
*/
200+
async findOrphanedChunks(options) {
201+
const service = await this.#getService();
202+
return await service.findOrphanedChunks(options);
203+
}
180204
}

src/domain/services/CasService.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,96 @@ export default class CasService {
242242
return { buffer, bytesWritten: buffer.length };
243243
}
244244

245+
/**
246+
* Reads a manifest from a Git tree OID.
247+
*
248+
* @param {Object} options
249+
* @param {string} options.treeOid - Git tree OID to read the manifest from
250+
* @returns {Promise<import('../value-objects/Manifest.js').default>}
251+
* @throws {CasError} MANIFEST_NOT_FOUND if no manifest entry exists in the tree
252+
* @throws {CasError} GIT_ERROR if the underlying Git command fails
253+
*/
254+
async readManifest({ treeOid }) {
255+
let entries;
256+
try {
257+
entries = await this.persistence.readTree(treeOid);
258+
} catch (err) {
259+
if (err instanceof CasError) { throw err; }
260+
throw new CasError(
261+
`Failed to read tree ${treeOid}: ${err.message}`,
262+
'GIT_ERROR',
263+
{ treeOid, originalError: err },
264+
);
265+
}
266+
267+
const manifestName = `manifest.${this.codec.extension}`;
268+
const manifestEntry = entries.find((e) => e.name === manifestName);
269+
270+
if (!manifestEntry) {
271+
throw new CasError(
272+
`No manifest entry (${manifestName}) found in tree ${treeOid}`,
273+
'MANIFEST_NOT_FOUND',
274+
{ treeOid, expectedName: manifestName },
275+
);
276+
}
277+
278+
let blob;
279+
try {
280+
blob = await this.persistence.readBlob(manifestEntry.oid);
281+
} catch (err) {
282+
if (err instanceof CasError) { throw err; }
283+
throw new CasError(
284+
`Failed to read manifest blob ${manifestEntry.oid}: ${err.message}`,
285+
'GIT_ERROR',
286+
{ treeOid, manifestOid: manifestEntry.oid, originalError: err },
287+
);
288+
}
289+
290+
const decoded = this.codec.decode(blob);
291+
return new Manifest(decoded);
292+
}
293+
294+
/**
295+
* Returns deletion metadata for an asset stored in a Git tree.
296+
* Does not perform any destructive Git operations.
297+
*
298+
* @param {Object} options
299+
* @param {string} options.treeOid - Git tree OID of the asset
300+
* @returns {Promise<{ chunksOrphaned: number, slug: string }>}
301+
* @throws {CasError} MANIFEST_NOT_FOUND if the tree has no manifest
302+
*/
303+
async deleteAsset({ treeOid }) {
304+
const manifest = await this.readManifest({ treeOid });
305+
return {
306+
slug: manifest.slug,
307+
chunksOrphaned: manifest.chunks.length,
308+
};
309+
}
310+
311+
/**
312+
* Aggregates referenced chunk blob OIDs across multiple stored assets.
313+
* Analysis only — does not delete or modify anything.
314+
*
315+
* @param {Object} options
316+
* @param {string[]} options.treeOids - Git tree OIDs to analyze
317+
* @returns {Promise<{ referenced: Set<string>, total: number }>}
318+
* @throws {CasError} MANIFEST_NOT_FOUND if any treeOid lacks a manifest
319+
*/
320+
async findOrphanedChunks({ treeOids }) {
321+
const referenced = new Set();
322+
let total = 0;
323+
324+
for (const treeOid of treeOids) {
325+
const manifest = await this.readManifest({ treeOid });
326+
for (const chunk of manifest.chunks) {
327+
referenced.add(chunk.blob);
328+
total += 1;
329+
}
330+
}
331+
332+
return { referenced, total };
333+
}
334+
245335
/**
246336
* Verifies the integrity of a stored file by re-hashing its chunks.
247337
* @param {import('../value-objects/Manifest.js').default} manifest

0 commit comments

Comments
 (0)