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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased] — M4 Compass

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

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

Expand Down
22 changes: 11 additions & 11 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ Return and throw semantics for every public method (current and planned).

| Version | Milestone | Codename | Theme |
|--------:|-----------|----------|-------|
| v1.1.0 | M1 | Bedrock | Foundation hardening |
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI |
| v1.3.0 | M3 | Launchpad| CI/CD pipeline |
| v1.4.0 | M4 | Compass | Lifecycle management |
| v1.1.0 | M1 | Bedrock | Foundation hardening | ✅ |
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI | ✅ |
| v1.3.0 | M3 | Launchpad| CI/CD pipeline | ✅ |
| v1.4.0 | M4 | Compass | Lifecycle management | ✅ |
Comment on lines +131 to +134
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Table column count mismatch — the ✅ creates a 5th cell outside the header.

The header defines 4 columns (Version | Milestone | Codename | Theme) but lines 131–134 each have 5 cells. Strict Markdown renderers will drop the extra cell or display it incorrectly. Move the checkmark into the existing Theme column instead.

Proposed fix
-| v1.1.0  | M1        | Bedrock  | Foundation hardening | ✅ |
-| v1.2.0  | M2        | Boomerang| File retrieval round trip + CLI | ✅ |
-| v1.3.0  | M3        | Launchpad| CI/CD pipeline | ✅ |
-| v1.4.0  | M4        | Compass  | Lifecycle management | ✅ |
+| v1.1.0  | M1        | Bedrock  | Foundation hardening ✅ |
+| v1.2.0  | M2        | Boomerang| File retrieval round trip + CLI ✅ |
+| v1.3.0  | M3        | Launchpad| CI/CD pipeline ✅ |
+| v1.4.0  | M4        | Compass  | Lifecycle management ✅ |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| v1.1.0 | M1 | Bedrock | Foundation hardening | |
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI | |
| v1.3.0 | M3 | Launchpad| CI/CD pipeline | |
| v1.4.0 | M4 | Compass | Lifecycle management | |
| v1.1.0 | M1 | Bedrock | Foundation hardening ✅ |
| v1.2.0 | M2 | Boomerang| File retrieval round trip + CLI ✅ |
| v1.3.0 | M3 | Launchpad| CI/CD pipeline ✅ |
| v1.4.0 | M4 | Compass | Lifecycle management ✅ |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 131-131: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 132-132: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 133-133: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)


[warning] 134-134: Table column count
Expected: 4; Actual: 5; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 Prompt for AI Agents
In `@ROADMAP.md` around lines 131 - 134, The table rows (e.g., the lines starting
with "v1.1.0", "v1.2.0", "v1.3.0", "v1.4.0") have an extra cell for the
checkmark, causing a 5th column while the header is "Version | Milestone |
Codename | Theme"; fix each row by removing the extra pipe and placing the
checkmark inside the existing Theme cell (so Theme contains "Foundation
hardening ✅", "File retrieval round trip + CLI ✅", etc.), ensuring each row has
exactly four cells to match the header.

| v1.5.0 | M5 | Sonar | Observability |
| v1.6.0 | M6 | Cartographer | Documentation |
| v2.0.0 | M7 | Horizon | Advanced features |
Expand Down Expand Up @@ -178,7 +178,7 @@ M3 Launchpad (v1.3.0) M4 Compass (v1.4.0)

---

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

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

---

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

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

---

# M3 — Launchpad (v1.3.0)
# M3 — Launchpad (v1.3.0)
**Theme:** Automated quality gates and release process.

---
Expand Down Expand Up @@ -1014,12 +1014,12 @@ As a maintainer, I want releases published automatically on version tags so publ

---

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

---

## Task 4.1: Implement readManifest() on CasService
## Task 4.1: Implement readManifest() on CasService

**User Story**
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.
Expand Down Expand Up @@ -1073,7 +1073,7 @@ As a developer, I want to reconstruct a Manifest from a Git tree OID so I can in

---

## Task 4.2: Implement deleteAsset() (logical unlink info)
## Task 4.2: Implement deleteAsset() (logical unlink info)

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

---

## Task 4.3: Implement orphaned chunk analysis
## Task 4.3: Implement orphaned chunk analysis

**User Story**
As an operator, I want to identify referenced chunks across many assets so I can assess storage waste.
Expand Down
24 changes: 24 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,28 @@ export default class ContentAddressableStore {
const service = await this.#getService();
return await service.verifyIntegrity(manifest);
}

/**
* Reads a manifest from a Git tree OID.
*/
async readManifest(options) {
const service = await this.#getService();
return await service.readManifest(options);
}

/**
* Returns deletion metadata for an asset stored in a Git tree.
*/
async deleteAsset(options) {
const service = await this.#getService();
return await service.deleteAsset(options);
}

/**
* Aggregates referenced chunk blob OIDs across multiple stored assets.
*/
async findOrphanedChunks(options) {
const service = await this.#getService();
return await service.findOrphanedChunks(options);
}
}
90 changes: 90 additions & 0 deletions src/domain/services/CasService.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,96 @@ export default class CasService {
return { buffer, bytesWritten: buffer.length };
}

/**
* Reads a manifest from a Git tree OID.
*
* @param {Object} options
* @param {string} options.treeOid - Git tree OID to read the manifest from
* @returns {Promise<import('../value-objects/Manifest.js').default>}
* @throws {CasError} MANIFEST_NOT_FOUND if no manifest entry exists in the tree
* @throws {CasError} GIT_ERROR if the underlying Git command fails
*/
async readManifest({ treeOid }) {
let entries;
try {
entries = await this.persistence.readTree(treeOid);
} catch (err) {
if (err instanceof CasError) { throw err; }
throw new CasError(
`Failed to read tree ${treeOid}: ${err.message}`,
'GIT_ERROR',
{ treeOid, originalError: err },
);
}

const manifestName = `manifest.${this.codec.extension}`;
const manifestEntry = entries.find((e) => e.name === manifestName);

if (!manifestEntry) {
throw new CasError(
`No manifest entry (${manifestName}) found in tree ${treeOid}`,
'MANIFEST_NOT_FOUND',
{ treeOid, expectedName: manifestName },
);
}

let blob;
try {
blob = await this.persistence.readBlob(manifestEntry.oid);
} catch (err) {
if (err instanceof CasError) { throw err; }
throw new CasError(
`Failed to read manifest blob ${manifestEntry.oid}: ${err.message}`,
'GIT_ERROR',
{ treeOid, manifestOid: manifestEntry.oid, originalError: err },
);
}

const decoded = this.codec.decode(blob);
return new Manifest(decoded);
}

/**
* Returns deletion metadata for an asset stored in a Git tree.
* Does not perform any destructive Git operations.
*
* @param {Object} options
* @param {string} options.treeOid - Git tree OID of the asset
* @returns {Promise<{ chunksOrphaned: number, slug: string }>}
* @throws {CasError} MANIFEST_NOT_FOUND if the tree has no manifest
*/
async deleteAsset({ treeOid }) {
const manifest = await this.readManifest({ treeOid });
return {
slug: manifest.slug,
chunksOrphaned: manifest.chunks.length,
};
}

/**
* Aggregates referenced chunk blob OIDs across multiple stored assets.
* Analysis only — does not delete or modify anything.
*
* @param {Object} options
* @param {string[]} options.treeOids - Git tree OIDs to analyze
* @returns {Promise<{ referenced: Set<string>, total: number }>}
* @throws {CasError} MANIFEST_NOT_FOUND if any treeOid lacks a manifest
*/
async findOrphanedChunks({ treeOids }) {
const referenced = new Set();
let total = 0;

for (const treeOid of treeOids) {
const manifest = await this.readManifest({ treeOid });
for (const chunk of manifest.chunks) {
referenced.add(chunk.blob);
total += 1;
}
}

return { referenced, total };
}

/**
* Verifies the integrity of a stored file by re-hashing its chunks.
* @param {import('../value-objects/Manifest.js').default} manifest
Expand Down
Loading