feat(slice-05): bundle-context-packs-router module — Item 6 cross-primitive bundling part 5b (Checkpoint 7.3f step 2) (OPR.0.3.2.5)

Pure-function router module for the context_packs cross-primitive kind.
Per PRD §Item 6 line 196 + grounded against context-pack-library-service
consumer contract: manifest declares context_packs entries as paths to
manifest.yaml files; router copies the PARENT DIRECTORY of each
manifest.yaml to <targetContextPacksDir>/<basename(parentDir)>/ (the
operator-host canonical layout the live consumer scans per context-pack-
types.ts:9-10).

Mirrors the bundle-plugins-router pattern (dir-based with copyDir
FsOps) plus the workflow_specs-router degenerate-input handling
(banked discipline 16ebb8af). Pre-banked the consumer-invisibility +
collision classes into the FIRST commit this time (avoiding the
7.3e-step3 multi-cycle catch sequence):

bundle-context-packs-router.ts (+208):
- ContextPacksRouterFsOps interface (exists, isDirectory, mkdirp, copyDir)
- 6 status enum values: routed / missing / unsafe / not_manifest /
  not_directory / conflict
- routeContextPacks pure function with:
  * Consumer-visibility prefilter: rejects declared paths whose basename
    is not "manifest.yaml" (status=not_manifest; consumer scans pack
    dirs for a top-level manifest.yaml — any other basename is invisible)
  * Source-side path-containment check
  * Pack source dir existence + isDirectory checks
  * Parent-dir-basename collision detection (routedDirNames Set) — first
    wins, second flagged status=conflict (mirror of workflow_specs
    basename-collision lesson on 16ebb8af)
  * Target-side path-containment check (structurally safe via basename;
    kept defensive)
  * copyDir(sourcePackDir, targetAbs) — dir-based routing

bundle-context-packs-router.test.ts (+207): 9 tests C1-C9 covering the
full envelope:
- C1 empty list
- C2 single pack routes end-to-end
- C3 multiple distinct packs route correctly
- C4 missing source dir → status=missing
- C5 source-side escape → status=unsafe
- C6 (degenerate-input): non-manifest.yaml basename → status=not_manifest
  (covers both .yaml-but-wrong-name and .txt extension)
- C7 (degenerate-input): parent-dir basename collision → 1st routed,
  2nd conflict; ONLY 1 copyDir fires
- C8 source parent is file not directory → status=not_directory
- C9 mixed list aggregating all 6 status classes

Discriminator-flips BOTH PROACTIVELY:

Flip A — remove the not-manifest.yaml prefilter. Result: 2/9 fail
(C6 + C9 mixed-list aggregate assertion). Proves prefilter specifically
catches the consumer-invisibility class. Restored.

Flip B — remove the routedDirNames collision detection. Result: 2/9
fail (C7 + C9 mixed-list aggregate assertion). Proves collision
detection specifically catches the false-positive routedCount class.
Restored.

Final: 34/34 across all 4 router modules:
- bundle-skills-router 8
- bundle-plugins-router 9
- bundle-workflow-specs-router 8
- bundle-context-packs-router 9 (NEW)

Per carry-forward from 329e2e22 guard CLEAR: manifest.yaml basename
check + parent-dir collision detection both ENFORCED at router level
ahead of /install integration. Step 3 will add the live-consumer
proof: invoke ContextPackLibraryService.scan() against the routed
target dir + assert the pack is visible with expected name+version
(mirror of workflow_specs scanWorkflowSpecFolder reachability proof
d81456dc).

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:36:08 -07:00
1 parent 329e2e228d
commit a0e7e0e196
2 files changed
+411

No files matched your search

@@ -0,0 +1,196 @@
/**
* Bundle context_packs router (Item 6 / slice-05 Checkpoint 7.3f step 2).
*
* Pure function. Copies context-pack directories declared in a bundle
* manifest's context_packs[] block from the bundle's extracted tree to
* the operator context-packs library. No daemon dependencies — fully
* unit-testable via FsOps injection.
*
* Per PRD §Item 6 line 196: context_packs entries are paths to a
* context-pack's manifest.yaml file inside the bundle. The PACK is the
* PARENT DIRECTORY of that manifest.yaml (per context-pack-types.ts:9-10
* and context-pack-library-service.ts:50-77: a pack is a directory whose
* immediate children include manifest.yaml + the referenced content
* files). The router copies that parent dir to
* <targetContextPacksDir>/<basename(parentDir)>/ — the operator-host
* canonical layout the live consumer scans.
*
* Banked degenerate-input dogfood discipline (extended from the
* workflow_specs basename-collision + non-YAML cycles): every router
* commit explicitly probes the failure-class edges through the live
* consumer's contract before claiming routedCount truthful. For
* context_packs the consumer-invisibility classes are:
* - Declared path basename is NOT "manifest.yaml" → consumer never
* scans (it looks for manifest.yaml at the pack dir root). status=
* not_manifest.
* - Two declared paths whose parent-dir basename collide → second
* would silently overwrite the first; status=conflict; first wins.
* - Source pack dir not present in bundle → status=missing (honest skip).
* - Source path escapes bundleRoot → status=unsafe.
*
* Both-sides containment (banked
* feedback_pre_existing_trust_boundary_reuse_canonical_helper addendum):
* source-side under bundleRoot, target-side under targetContextPacksDir
* (target via basename so structurally safe; defensive check retained).
*
* /install handler integration lands at Checkpoint 7.3f step 3 with a
* real consumer-scan() proof against the routed dir (mirror of the
* workflow_specs scanWorkflowSpecFolder reachability proof at d81456dc).
*/
import nodePath from "node:path";
/** Filesystem injection point — real impl wraps node:fs. Tests substitute in-memory. */
export interface ContextPacksRouterFsOps {
exists: (path: string) => boolean;
isDirectory: (path: string) => boolean;
mkdirp: (path: string) => void;
copyDir: (src: string, dest: string) => void;
}
/** Inputs to routeContextPacks. */
export interface RouteContextPacksInput {
/** Absolute path to the bundle's extracted root (tmp dir from unpack). */
bundleRoot: string;
/** Relative paths to context-pack manifest.yaml files declared in the
* bundle manifest's context_packs[] block. */
declaredContextPacks: string[];
/** Absolute path to the operator context-packs library (per
* context-pack-types.ts:9-10: typically `<openrigHome>/context-packs`
* or a workspace-local `.openrig/context-packs`). Caller resolves. */
targetContextPacksDir: string;
}
/** One routed context_pack (or one rejection). */
export interface RoutedContextPackRecord {
/** Declared path from manifest.context_packs[]. */
declaredPath: string;
/** "routed" = pack dir copied successfully and consumer-visible.
* "missing" = pack manifest source not in bundle (honest skip).
* "unsafe" = source escapes bundleRoot OR target escapes
* targetContextPacksDir (structurally impossible given basename, kept
* defensive).
* "not_manifest" = declared path basename is not "manifest.yaml" —
* consumer (context-pack-library-service.scan) scans pack dirs for a
* top-level manifest.yaml file; any other basename is invisible-by-
* construction.
* "not_directory" = parent of the declared manifest path is not a
* directory in the bundle tree.
* "conflict" = parent-dir basename collides with an earlier routed
* pack; first wins, second flagged so routedCount stays truthful at
* the consumer-visible boundary (banked 16ebb8af lesson). */
status: "routed" | "missing" | "unsafe" | "not_manifest" | "not_directory" | "conflict";
/** Where the pack landed in the target library (absolute pack dir
* path), if routed. */
installedAt?: string;
/** Human-readable detail (3-part error shape input for caller). */
detail?: string;
}
/** Aggregate routing result. */
export interface RouteContextPacksResult {
records: RoutedContextPackRecord[];
routedCount: number;
rejectedCount: number;
}
/**
* Route each declared context_pack from the bundle tree to the operator
* context-packs library. Per-entry safety: SOURCE containment under
* bundleRoot + TARGET containment under targetContextPacksDir.
* Degenerate inputs the consumer ignores (non-manifest.yaml basename;
* parent-dir basename collisions) are caught BEFORE write so the
* routedCount stays truthful at the consumer-visible boundary. Caller
* writes the install audit record using the records returned here.
*/
export function routeContextPacks(
input: RouteContextPacksInput,
fs: ContextPacksRouterFsOps,
): RouteContextPacksResult {
const records: RoutedContextPackRecord[] = [];
const bundleRootResolved = nodePath.resolve(input.bundleRoot);
const targetRootResolved = nodePath.resolve(input.targetContextPacksDir);
fs.mkdirp(input.targetContextPacksDir);
// Track parent-dir basenames already routed so collisions surface as
// conflict records, not silent overwrites (banked workflow_specs lesson
// 16ebb8af).
const routedDirNames = new Set<string>();
for (const declared of input.declaredContextPacks) {
// Consumer-visibility prefilter: context-pack-library-service scans
// pack dirs whose immediate child is manifest.yaml. Any declared path
// whose basename isn't "manifest.yaml" routes to a dir the consumer
// will never recognize as a pack. Reject pre-write so routedCount
// stays truthful at the consumer-visible boundary.
if (nodePath.basename(declared) !== "manifest.yaml") {
records.push({
declaredPath: declared,
status: "not_manifest",
detail: `context_pack '${declared}' basename is not 'manifest.yaml'; context-pack-library scans pack dirs for a top-level manifest.yaml`,
});
continue;
}
const sourceAbs = nodePath.resolve(input.bundleRoot, declared);
// Defense-in-depth path-containment on SOURCE (mirrors skills/
// workflow_specs routers).
if (sourceAbs !== bundleRootResolved && !sourceAbs.startsWith(bundleRootResolved + nodePath.sep)) {
records.push({
declaredPath: declared,
status: "unsafe",
detail: `context_pack path '${declared}' escapes bundle workspace; rejected`,
});
continue;
}
const sourcePackDir = nodePath.dirname(sourceAbs);
if (!fs.exists(sourcePackDir)) {
records.push({
declaredPath: declared,
status: "missing",
detail: `context_pack source dir '${nodePath.dirname(declared)}' not present in bundle; skipped`,
});
continue;
}
if (!fs.isDirectory(sourcePackDir)) {
records.push({
declaredPath: declared,
status: "not_directory",
detail: `context_pack parent of '${declared}' is not a directory; skipped`,
});
continue;
}
const dirName = nodePath.basename(sourcePackDir);
// Target = <targetContextPacksDir>/<basename(parentDir)>/ (the operator-
// host canonical layout per context-pack-types.ts:9-10).
const targetAbs = nodePath.resolve(input.targetContextPacksDir, dirName);
// Sanity check (basename is structurally safe but the resolve could
// theoretically be a no-op for "" → keep the check, expected always-
// true).
if (targetAbs !== targetRootResolved && !targetAbs.startsWith(targetRootResolved + nodePath.sep)) {
records.push({
declaredPath: declared,
status: "unsafe",
detail: `context_pack target path for '${declared}' escapes target context-packs library; rejected`,
});
continue;
}
if (routedDirNames.has(dirName)) {
records.push({
declaredPath: declared,
status: "conflict",
detail: `context_pack parent-dir basename '${dirName}' collides with an earlier declared path; only the first is routed (banked collision-detection lesson)`,
});
continue;
}
fs.copyDir(sourcePackDir, targetAbs);
routedDirNames.add(dirName);
records.push({
declaredPath: declared,
status: "routed",
installedAt: targetAbs,
});
}
const routedCount = records.filter((r) => r.status === "routed").length;
const rejectedCount = records.length - routedCount;
return { records, routedCount, rejectedCount };
}
@@ -0,0 +1,215 @@
import { describe, it, expect } from "vitest";
import { routeContextPacks, type ContextPacksRouterFsOps, type RouteContextPacksInput } from "../src/domain/bundle-context-packs-router.js";
// Item 6 / slice-05 Checkpoint 7.3f step 2: bundle-context-packs-router
// pure-function tests. Mirrors plugins router (dir-based) with the
// context-pack-specific degenerate-input handling per the consumer
// contract (context-pack-library-service.scan walks pack dirs whose
// immediate child is manifest.yaml).
function mockFs(initial: { dirs?: string[] } = {}): ContextPacksRouterFsOps & {
_copyCalls: Array<{ src: string; dest: string }>;
_mkdirpCalls: string[];
_dirs: Set<string>;
} {
const dirs = new Set<string>(initial.dirs ?? []);
const copyCalls: Array<{ src: string; dest: string }> = [];
const mkdirpCalls: string[] = [];
return {
_copyCalls: copyCalls,
_mkdirpCalls: mkdirpCalls,
_dirs: dirs,
exists: (p: string) => dirs.has(p),
isDirectory: (p: string) => dirs.has(p),
mkdirp: (p: string) => { mkdirpCalls.push(p); dirs.add(p); },
copyDir: (src: string, dest: string) => { copyCalls.push({ src, dest }); dirs.add(dest); },
};
}
const BUNDLE_ROOT = "/bundle/root";
const TARGET = "/operator/.openrig/context-packs";
function makeInput(overrides?: Partial<RouteContextPacksInput>): RouteContextPacksInput {
return {
bundleRoot: BUNDLE_ROOT,
declaredContextPacks: [],
targetContextPacksDir: TARGET,
...overrides,
};
}
describe("routeContextPacks", () => {
// C1: empty list → empty records + target dir mkdirp'd
it("empty declaredContextPacks produces empty records but still mkdirp's target", () => {
const fs = mockFs();
const result = routeContextPacks(makeInput(), fs);
expect(result.records).toEqual([]);
expect(result.routedCount).toBe(0);
expect(result.rejectedCount).toBe(0);
expect(fs._mkdirpCalls).toContain(TARGET);
});
// C2: routes one pack: copyDir from sourceParentDir to target/<dirname>
it("routes one context_pack: parent dir copied to target/<dirname>", () => {
const fs = mockFs({ dirs: [`${BUNDLE_ROOT}/context-packs/intent`] });
const result = routeContextPacks(
makeInput({ declaredContextPacks: ["context-packs/intent/manifest.yaml"] }),
fs,
);
expect(result.routedCount).toBe(1);
expect(result.records[0]!.status).toBe("routed");
expect(result.records[0]!.installedAt).toBe(`${TARGET}/intent`);
expect(fs._copyCalls).toHaveLength(1);
expect(fs._copyCalls[0]).toEqual({
src: `${BUNDLE_ROOT}/context-packs/intent`,
dest: `${TARGET}/intent`,
});
});
// C3: multiple distinct packs route correctly
it("routes multiple distinct context_packs each to target/<dirname>", () => {
const fs = mockFs({
dirs: [`${BUNDLE_ROOT}/context-packs/intent`, `${BUNDLE_ROOT}/context-packs/persona`],
});
const result = routeContextPacks(
makeInput({
declaredContextPacks: [
"context-packs/intent/manifest.yaml",
"context-packs/persona/manifest.yaml",
],
}),
fs,
);
expect(result.routedCount).toBe(2);
expect(fs._copyCalls).toHaveLength(2);
});
// C4: missing source pack dir skipped honestly
it("missing source pack dir → status=missing (honest-scoping)", () => {
const fs = mockFs();
const result = routeContextPacks(
makeInput({ declaredContextPacks: ["context-packs/absent/manifest.yaml"] }),
fs,
);
expect(result.routedCount).toBe(0);
expect(result.records[0]!.status).toBe("missing");
expect(result.records[0]!.detail).toContain("not present");
expect(fs._copyCalls).toHaveLength(0);
});
// C5: unsafe source path escaping bundle workspace rejected
it("unsafe source path escaping bundle workspace → status=unsafe", () => {
const fs = mockFs();
const result = routeContextPacks(
makeInput({ declaredContextPacks: ["../escape/manifest.yaml"] }),
fs,
);
expect(result.records[0]!.status).toBe("unsafe");
expect(result.records[0]!.detail).toContain("escapes bundle workspace");
expect(fs._copyCalls).toHaveLength(0);
});
// C6 (degenerate-input): basename not manifest.yaml — consumer-invisible
// class. Per banked PRE-handoff degenerate-input dogfood discipline.
it("declared path with non-manifest.yaml basename → status=not_manifest", () => {
const fs = mockFs({ dirs: [`${BUNDLE_ROOT}/context-packs/oddpack`] });
const result = routeContextPacks(
makeInput({
declaredContextPacks: [
"context-packs/oddpack/pack.yaml",
"context-packs/oddpack/manifest.txt",
],
}),
fs,
);
expect(result.records).toHaveLength(2);
expect(result.routedCount).toBe(0);
expect(result.records[0]!.status).toBe("not_manifest");
expect(result.records[0]!.detail).toContain("manifest.yaml");
expect(result.records[1]!.status).toBe("not_manifest");
expect(fs._copyCalls).toHaveLength(0);
});
// C7 (degenerate-input): parent-dir basename collision — first wins,
// second flagged conflict. Banked workflow_specs B1 lesson applied.
it("two declared packs sharing parent-dir basename → first routed, second conflict", () => {
const fs = mockFs({
dirs: [
`${BUNDLE_ROOT}/a/intent`,
`${BUNDLE_ROOT}/b/intent`,
],
});
const result = routeContextPacks(
makeInput({
declaredContextPacks: [
"a/intent/manifest.yaml",
"b/intent/manifest.yaml",
],
}),
fs,
);
expect(result.records).toHaveLength(2);
expect(result.routedCount).toBe(1);
expect(result.rejectedCount).toBe(1);
expect(result.records[0]!.status).toBe("routed");
expect(result.records[0]!.installedAt).toBe(`${TARGET}/intent`);
expect(result.records[1]!.status).toBe("conflict");
expect(result.records[1]!.detail).toContain("intent");
expect(result.records[1]!.detail).toContain("collides");
// Only the first copyDir fires.
expect(fs._copyCalls).toHaveLength(1);
expect(fs._copyCalls[0]!.src).toBe(`${BUNDLE_ROOT}/a/intent`);
});
// C8: pack source path exists but is a file not a directory — edge case
// (unusual; manifest.yaml dirname resolved to a file). Skipped honestly.
it("source parent exists but is a file (not directory) → status=not_directory", () => {
// mockFs treats anything in dirs[] as a dir; for this case construct
// an exists-but-not-isDirectory state by overriding isDirectory.
const fs = mockFs();
fs._dirs.add(`${BUNDLE_ROOT}/oddpath`); // exists
// Override isDirectory just for this path:
const origIsDir = fs.isDirectory;
fs.isDirectory = (p: string) => p === `${BUNDLE_ROOT}/oddpath` ? false : origIsDir(p);
const result = routeContextPacks(
makeInput({ declaredContextPacks: ["oddpath/manifest.yaml"] }),
fs,
);
expect(result.records[0]!.status).toBe("not_directory");
expect(fs._copyCalls).toHaveLength(0);
});
// C9: mixed list — routed + missing + unsafe + not_manifest + conflict
// aggregate correctly
it("mixed declared list aggregates correctly across all rejection classes", () => {
const fs = mockFs({
dirs: [
`${BUNDLE_ROOT}/context-packs/ok`,
`${BUNDLE_ROOT}/context-packs/dup`,
`${BUNDLE_ROOT}/elsewhere/dup`,
],
});
const result = routeContextPacks(
makeInput({
declaredContextPacks: [
"context-packs/ok/manifest.yaml", // routed
"context-packs/absent/manifest.yaml", // missing
"../escape/manifest.yaml", // unsafe
"context-packs/odd/pack.yaml", // not_manifest
"context-packs/dup/manifest.yaml", // routed (1st dup)
"elsewhere/dup/manifest.yaml", // conflict (basename dup with above)
],
}),
fs,
);
expect(result.records).toHaveLength(6);
expect(result.routedCount).toBe(2);
expect(result.rejectedCount).toBe(4);
expect(result.records[0]!.status).toBe("routed");
expect(result.records[1]!.status).toBe("missing");
expect(result.records[2]!.status).toBe("unsafe");
expect(result.records[3]!.status).toBe("not_manifest");
expect(result.records[4]!.status).toBe("routed");
expect(result.records[5]!.status).toBe("conflict");
});
});