src/portfolio-docs.ts
89 lines and 8 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| isPortfolioPath | 38 to 51 | 13 | 100% | WatchCRAP 13 | No data: mutation testing has not run on this repo yet |
| isPortfolioTool | 54 to 56 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| describePortfolioDocs | 59 to 65 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:62> | 62 to 62 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:63> | 63 to 63 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| portfolioDocuments | 82 to 95 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:83> | 83 to 83 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:94> | 94 to 94 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. Imported by src/scope.ts, src/tools/docs-read.ts.
Source
// The portfolio documents every namespace driver may READ in capsid, and nothing else
// there.
//
// Every repo's CLAUDE.md tells a session to follow capsid/conventions.md and Dustin's
// rulings, but a driver minted for one namespace was refused all of
// them, so it worked from its repo's CLAUDE.md and the job bodies alone. This is the
// one statement of what crosses that boundary. checkScope (src/scope.ts) admits a read
// of a path matched here, the list, glob and search handlers drop every other capsid
// row for such a caller, and the session start names the documents that exist. Nothing else in
// capsid (research, policy, jobs, reports, episodics) is reachable, and no write is.
//
// test/portfolio-docs.test.ts pins this constant.
export const PORTFOLIO_DOCS = {
namespace: "capsid",
// Exact paths. conventions.md is the one rules file.
documents: ["conventions.md", "core.md"],
// An archived decisions volume: the prefix, then a volume number, then ".md". The
// old decisions log is history kept under archive/; new rulings go in rulings/.
volumes: ["archive/decisions-vol-"],
// One document directly inside the folder, no deeper. One ruling per file.
folders: ["rulings/"],
// The tool actions that may reach capsid for such a caller, as tool.action. Each
// requires only the read grant (TOOL_GRANTS). A document's versions and links stay
// out: a version list or an edge can name a capsid path that is not on this list.
tools: ["capsid_read_doc.read", "capsid_find_docs.list", "capsid_find_docs.glob", "capsid_find_docs.search"],
} as const;
// A path is matched against this before anything else: lowercase letters, digits, dot,
// dash, underscore and slash only. So a percent-encoding, a backslash, a space or a
// capital letter cannot reach a comparison, whatever the database or a later decoder
// would make of it. Stored paths are case-sensitive, and every portfolio path is
// lowercase.
const SAFE_PATH = /^[a-z0-9][a-z0-9._/-]*$/;
const VOLUME = /^[1-9][0-9]{0,3}\.md$/;
const FOLDER_ENTRY = /^[a-z0-9][a-z0-9._-]*\.md$/;
/** Whether a capsid path is one of the portfolio documents. */
export function isPortfolioPath(path: string): boolean {
if (typeof path !== "string" || !SAFE_PATH.test(path)) return false;
// No traversal and no empty segment, even though nothing here resolves a path: the
// test is on the string, and "rulings/../policy/gates.md" must not read as rulings/.
if (path.includes("..") || path.includes("//") || path.endsWith("/")) return false;
if ((PORTFOLIO_DOCS.documents as readonly string[]).includes(path)) return true;
for (const prefix of PORTFOLIO_DOCS.volumes) {
if (path.startsWith(prefix) && VOLUME.test(path.slice(prefix.length))) return true;
}
for (const folder of PORTFOLIO_DOCS.folders) {
if (path.startsWith(folder) && FOLDER_ENTRY.test(path.slice(folder.length))) return true;
}
return false;
}
/** Whether a tool action may reach the portfolio documents for a caller outside capsid. */
export function isPortfolioTool(tool: string, action: string | undefined): boolean {
return action !== undefined && (PORTFOLIO_DOCS.tools as readonly string[]).includes(`${tool}.${action}`);
}
/** The allowlist as a reader sees it, for a refusal and for brief. */
export function describePortfolioDocs(): string[] {
return [
...PORTFOLIO_DOCS.documents,
...PORTFOLIO_DOCS.volumes.map((v) => `${v}<n>.md`),
...PORTFOLIO_DOCS.folders.map((f) => `${f}<name>.md`),
];
}
export interface PortfolioRow {
namespace: string;
path: string;
title: string | null;
type: string | null;
status: string | null;
tags: string | null;
created_at: string;
updated_at: string;
}
/** The portfolio documents that exist, metadata only, ordered by path. The query
* narrows by the exact paths and a GLOB per volume prefix and folder; isPortfolioPath
* then decides, so the GLOB's looser match (a nested rulings path, a non-numeric
* volume) never reaches a caller. */
export async function portfolioDocuments(db: D1Database): Promise<PortfolioRow[]> {
const globs = [...PORTFOLIO_DOCS.volumes, ...PORTFOLIO_DOCS.folders].map((p) => `${p}*`);
const { results } = await db
.prepare(
`SELECT namespace, path, title, type, status, tags, created_at, updated_at
FROM documents
WHERE namespace = ?1
AND (path IN (SELECT value FROM json_each(?2)) OR EXISTS (SELECT 1 FROM json_each(?3) AS g WHERE documents.path GLOB g.value))
ORDER BY path`
)
.bind(PORTFOLIO_DOCS.namespace, JSON.stringify(PORTFOLIO_DOCS.documents), JSON.stringify(globs))
.all<PortfolioRow>();
return results.filter((r) => isPortfolioPath(r.path));
}