@@ -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