fix(slice-05): context_packs reject manifest-as-directory (guard B2 repair on d491eca9) (OPR.0.3.2.5)

Per velocity-guard BLOCKING-CONCERN on d491eca9 (qitem-…5bd8de60): the
manifest-file existence check uses fs.exists() which returns true for
BOTH files and directories. A bundle could declare context-packs/foo/
manifest.yaml and include manifest.yaml as a DIRECTORY (not a file).
exists() passes; the live consumer's readFileSync(manifestPath)
(context-pack-library-service.ts:135) throws on a dir; scan() records
an error diagnostic instead of indexing a visible pack. Same false-
positive routedCount class at the file-vs-directory discrimination axis
guard banked in the d491eca9 cycle.

Fix: minimal `if (fs.isDirectory(sourceAbs))` check before treating the
declared path as routable. Status reuses not_manifest with explicit
"exists but is a directory, not a file" detail. No FsOps extension
needed.

bundle-context-packs-router.ts +8/-1: new isDirectory branch between
the manifest-file existence check and the parent-dir isDirectory check.

bundle-context-packs-router.test.ts +28/-0: C11 — parent dir exists +
sourceAbs exists as DIRECTORY (not file) → routedCount=0,
rejectedCount=1, status=not_manifest, NO copyDir.

Discriminator-flip BEFORE this commit (PROACTIVELY): removed the new
isDirectory branch. Result: 1/11 fail — exactly C11. Clean targeted
discriminator (the check is structurally specific to the manifest-is-
directory case). Restored, 11/11 + 152/152 across full slice-05 daemon
suite.

Ninth same-class catch on slice-05's tail. The exists/isDirectory
distinction was the SECOND file-vs-dir discrimination axis the consumer
uses; my B1 repair fixed file-presence but missed file-vs-dir type
discrimination. Banked discipline extension: when a mock supports
files-vs-dirs as separate sets (per d491eca9 lesson), ALL fs operations
used by the router need to be checked against BOTH axes — exists check
alone is insufficient because it crosses the file/dir boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-31 19:45:12 -07:00
1 parent d491eca96e
commit 3cd581e3c4
2 files changed
+44

No files matched your search

@@ -156,6 +156,21 @@ export function routeContextPacks(
});
continue;
}
// Manifest-type check (banked B2 from d491eca9 guard catch): exists()
// returns true for both files AND directories. If sourceAbs exists as
// a directory (named manifest.yaml, but a dir not a file), the live
// consumer's readFileSync(manifestPath) throws and scan() records an
// error diagnostic instead of indexing a visible pack — false-positive
// routedCount class. Reject pre-write so routedCount stays truthful at
// the consumer-visible boundary.
if (fs.isDirectory(sourceAbs)) {
records.push({
declaredPath: declared,
status: "not_manifest",
detail: `context_pack manifest '${declared}' exists but is a directory, not a file; consumer requires manifest.yaml to be a readable file`,
});
continue;
}
const sourcePackDir = nodePath.dirname(sourceAbs);
if (!fs.isDirectory(sourcePackDir)) {
records.push({
@@ -226,6 +226,35 @@ describe("routeContextPacks", () => {
expect(result.records[5]!.status).toBe("conflict");
});
// C11 (degenerate-input / banked guard catch on d491eca9):
// manifest.yaml exists at the declared path but is a DIRECTORY, not a
// file. fs.exists returns true for both shapes; the live consumer
// (context-pack-library-service.ts:135 readFileSync) throws on a dir
// path and scan() records an error diagnostic instead of indexing the
// pack. Router must reject pre-write so routedCount stays truthful.
it("manifest.yaml exists but is a directory → status=not_manifest (no false-positive routed)", () => {
const fs = mockFs({
// Parent dir IS a directory, AND manifest.yaml is ALSO a directory
// (not a file). The bare exists check would pass; isDirectory check
// catches this.
dirs: [
`${BUNDLE_ROOT}/context-packs/dirpack`,
`${BUNDLE_ROOT}/context-packs/dirpack/manifest.yaml`,
],
});
const result = routeContextPacks(
makeInput({ declaredContextPacks: ["context-packs/dirpack/manifest.yaml"] }),
fs,
);
expect(result.records).toHaveLength(1);
expect(result.routedCount).toBe(0);
expect(result.rejectedCount).toBe(1);
expect(result.records[0]!.status).toBe("not_manifest");
expect(result.records[0]!.detail).toContain("directory");
// CRUCIAL: no copyDir fires (consumer-invisible pack must not route)
expect(fs._copyCalls).toHaveLength(0);
});
// C10 (degenerate-input / banked guard catch on a0e7e0e1):
// parent dir exists but manifest.yaml file itself is absent —
// ContextPackLibraryService.scan skips packs missing manifest.yaml