openspec/initiatives/context-store-and-initiatives/work-items/04-add-collection-foundation/plan.md
First implementation slice implemented.
Start from ../../direction.md.
The relevant model is:
Context stores sync truth.
Collections shape truth.
Initiatives coordinate work.
Workspaces open local views.
Changes implement repo-owned slices.
Add the smallest collection foundation that lets product-specific content systems mount inside a context store without making the context-store layer know what those systems mean.
read, write, list, or delete helpers.initiatives/ can mount through generic collection definitions, not
through initiative-specific context-store logic.Use the module/object boundary to carry context instead of growing helper names.
Use a focused generic module such as src/core/collections/runtime.ts with
short names:
validateCollectionId(id);
validateMount(mount);
parseCollectionPath(input);
createCollectionRegistry(...);
mountCollections(...);
Prefer mounted objects for context-aware operations:
const mounted = collections.require("initiatives");
mounted.resolvePath("launch-billing-flow/initiative.yaml");
mounted.toStorePath("launch-billing-flow/initiative.yaml");
Avoid names like validateContextStoreCollectionRelativePath. They indicate
that too much context has leaked into a standalone helper name.
The first slice should stay close to this:
interface CollectionDefinition<THandle = unknown> {
id: string;
mount: string;
metadata?: CollectionMetadata;
hooks?: CollectionHooks;
createHandle?: (context: MountedCollectionContext) => THandle;
}
interface MountedCollectionContext {
storeRoot: string;
collectionId: string;
mount: string;
mountRoot: string;
resolvePath(relativePath?: string): string;
toStorePath(relativePath?: string): string;
}
interface MountedCollection<THandle = unknown> {
collectionId: string;
mount: string;
mountRoot: string;
context: MountedCollectionContext;
handle: THandle | undefined;
}
Use id on definitions, but collectionId on mounted handles and contexts so
domain object IDs such as initiative IDs do not collide with collection type IDs.
Use two separate layers:
Registration should hide persisted YAML details:
const store = await registerContextStore({
id: "acme-context",
backend: gitLocalBackend({
localPath: "/Users/me/repos/acme-context",
remote: "[email protected]:acme/context.git",
branch: "main",
}),
});
The registration facade can call lower-level helpers such as backend config
normalization, metadata writes, and local registry writes internally. Public
examples should not call raw writeContextStoreMetadataState(...),
writeContextStoreRegistryState(...), or expose persisted snake_case backend
state such as local_path.
Item 4 mounting should stay independent of registration and accept only the authority it needs:
const collections = createCollectionRegistry([
{ id: "initiatives", mount: "initiatives" },
]);
const mounted = mountCollections({
storeRoot: store.storeRoot,
collections,
});
mounted.require("initiatives").resolvePath(
"launch-billing-flow/initiative.yaml"
);
Prefer mountCollections({ storeRoot, collections }) as the canonical first
API. Passing a whole store handle can wait until there is a real need.
initiatives,
decisions, or api-catalog.Reject:
....openspec-storesetupContextStore({ id, backend, collections }) APIs.createStore(...).setup() lifecycle APIs.src/core/collections/runtime.ts.src/core/collections/index.ts and
src/core/index.ts.test/core/collections/runtime.test.ts.{ id: "initiatives", mount: "initiatives" } definition can
mount and resolve paths without initiative-specific store logic.