fix: tighten decision center query paths
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
function source(path: string): string {
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
function includesAll(text: string, snippets: string[]): boolean {
|
||||
return snippets.every((snippet) => text.includes(snippet));
|
||||
}
|
||||
|
||||
export function runDecisionCenterQueryContract(): JsonRecord {
|
||||
const service = source("src/components/microservices/decision-center/src/index.ts");
|
||||
const cli = source("scripts/src/decision-center.ts");
|
||||
const frontend = source("src/components/frontend/src/decision-center.tsx");
|
||||
|
||||
assertCondition(
|
||||
includesAll(service, [
|
||||
'url.pathname === "/api/requirements" && method === "GET"',
|
||||
"listRecords(url, { requirementOnly: true })",
|
||||
"type IN ('decision', 'goal', 'external_goal', 'internal_goal', 'blocker', 'debt', 'experiment')",
|
||||
]),
|
||||
"requirements list route must stay on the records model and exclude meetings",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(service, [
|
||||
"CASE WHEN ${includeBody}::boolean THEN body ELSE left(body, 4000) END AS body",
|
||||
"return rows.map((row) => recordFromRow(row, { includeBody }));",
|
||||
"body: includeBody ? body : \"\"",
|
||||
]),
|
||||
"record list must be body-light by default while preserving summaries",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(service, [
|
||||
"sourceFileFilterFromUrl(url)",
|
||||
"url.searchParams.get(\"sourceFile\") ?? url.searchParams.get(\"sourcePath\") ?? url.searchParams.get(\"source\")",
|
||||
"AND (${sourceFile || null}::text IS NULL OR source_file = ${sourceFile || null})",
|
||||
"getDiaryEntry(key, { sourceFile: sourceFileFilterFromUrl(url) })",
|
||||
]),
|
||||
"diary date lookup must support sourceFile disambiguation for same-day entries",
|
||||
);
|
||||
assertCondition(
|
||||
service.split("AND (${sourceFile || null}::text IS NULL OR source_file = ${sourceFile || null})").length >= 3,
|
||||
"diary sourceFile filter must cover both read and date-key upsert lookup paths",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(service, [
|
||||
"CASE WHEN ${includeBody}::boolean THEN body ELSE left(body, 4000) END AS body",
|
||||
"return rows.map((row) => diaryEntryFromRow(row, { includeBody }));",
|
||||
]),
|
||||
"diary list must be body-light by default while preserving summaries",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(cli, [
|
||||
"if (args.includes(\"--include-body\")) params.set(\"includeBody\", \"true\")",
|
||||
"function diaryShowQuery(args: string[]): string",
|
||||
"params.set(\"sourceFile\", sourceFile)",
|
||||
"showDiary(diaryId, args.slice(3))",
|
||||
"`/api/requirements${query ? `?${query}` : \"\"}`",
|
||||
]),
|
||||
"CLI must expose bounded list opt-in and diary source disambiguation",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(frontend, [
|
||||
"function diaryEntryLookupPath(entry: any): string",
|
||||
"const key = entry?.id || entry?.date",
|
||||
"if (entry?.sourceFile) params.set(\"sourceFile\", String(entry.sourceFile))",
|
||||
"decisionApi(apiBaseUrl, diaryEntryLookupPath(entry))",
|
||||
"if (!record?.id || record?.body) return",
|
||||
"`/api/records/${encodeURIComponent(record.id)}`",
|
||||
]),
|
||||
"frontend must select exact diary rows and fetch full record bodies before editing list results",
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"requirements-route",
|
||||
"body-light-record-list-query",
|
||||
"body-light-diary-list-query",
|
||||
"diary-source-disambiguation",
|
||||
"cli-bounded-list-and-diary-source-query",
|
||||
"frontend-exact-diary-row-and-record-edit-body",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(runDecisionCenterQueryContract(), null, 2)}\n`);
|
||||
}
|
||||
@@ -258,6 +258,7 @@ function recordQuery(args: string[], options: { requirementOnly?: boolean } = {}
|
||||
if (tag !== undefined) params.set("tag", tag);
|
||||
if (queryText !== undefined) params.set("q", queryText);
|
||||
if (limit !== undefined) params.set("limit", limit);
|
||||
if (args.includes("--include-body")) params.set("includeBody", "true");
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
@@ -302,14 +303,22 @@ async function listDiaryMonthsAsync(fetcher: (path: string, init?: { method?: st
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/months"));
|
||||
}
|
||||
|
||||
function showDiary(key: string | undefined): unknown {
|
||||
if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date");
|
||||
return unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}`));
|
||||
function diaryShowQuery(args: string[]): string {
|
||||
const params = new URLSearchParams();
|
||||
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
|
||||
if (sourceFile !== undefined) params.set("sourceFile", sourceFile);
|
||||
const query = params.toString();
|
||||
return query ? `?${query}` : "";
|
||||
}
|
||||
|
||||
async function showDiaryAsync(key: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
function showDiary(key: string | undefined, args: string[] = []): unknown {
|
||||
if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date");
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}`));
|
||||
return unwrapProxyResponse(decisionProxy(`/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`));
|
||||
}
|
||||
|
||||
async function showDiaryAsync(key: string | undefined, fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>, args: string[] = []): Promise<unknown> {
|
||||
if (!key) throw new Error("decision diary show requires entry id or YYYY-MM-DD date");
|
||||
return unwrapProxyResponse(await decisionProxyAsync(fetcher, `/api/diary/entries/${encodeURIComponent(key)}${diaryShowQuery(args)}`));
|
||||
}
|
||||
|
||||
function todayDiary(): unknown {
|
||||
@@ -460,7 +469,7 @@ export async function runDecisionCenterCommand(_config: UniDeskConfig, args: str
|
||||
if (diaryAction === "history") return diaryHistory(args.slice(2));
|
||||
if (diaryAction === "months") return listDiaryMonths();
|
||||
if (diaryAction === "today") return args.includes("--edit") ? editTodayDiary(args.slice(2).filter((arg) => arg !== "--edit")) : todayDiary();
|
||||
if (diaryAction === "show") return showDiary(diaryId);
|
||||
if (diaryAction === "show") return showDiary(diaryId, args.slice(3));
|
||||
if (diaryAction === "edit" || diaryAction === "upsert") return editDiary(args.slice(2));
|
||||
throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert");
|
||||
}
|
||||
@@ -496,7 +505,7 @@ export async function runDecisionCenterCommandAsync(
|
||||
if (diaryAction === "history") return diaryHistoryAsync(args.slice(2), fetcher);
|
||||
if (diaryAction === "months") return listDiaryMonthsAsync(fetcher);
|
||||
if (diaryAction === "today") return args.includes("--edit") ? editTodayDiaryAsync(args.slice(2).filter((arg) => arg !== "--edit"), fetcher) : todayDiaryAsync(fetcher);
|
||||
if (diaryAction === "show") return showDiaryAsync(diaryId, fetcher);
|
||||
if (diaryAction === "show") return showDiaryAsync(diaryId, fetcher, args.slice(3));
|
||||
if (diaryAction === "edit" || diaryAction === "upsert") return editDiaryAsync(args.slice(2), fetcher);
|
||||
throw new Error("decision diary command must be one of: import, list, history, months, today, show, edit, upsert");
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,9 +35,9 @@ export function rootHelp(): unknown {
|
||||
{ command: "decision diary history [--month YYYY-MM] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--limit N] [--include-body]", description: "Read diary history through the productized history API alias." },
|
||||
{ command: "decision diary today [--edit --body-file path] [--title text] [--tag tag]", description: "Get or create today's diary entry using the service's real current date; --edit saves today's Markdown." },
|
||||
{ command: "decision diary months", description: "List available Decision Center diary months with day counts." },
|
||||
{ command: "decision diary show <YYYY-MM-DD|id>", description: "Show one daily diary Markdown entry." },
|
||||
{ command: "decision diary show <YYYY-MM-DD|id> [--source-file path]", description: "Show one daily diary Markdown entry; source-file disambiguates same-day entries from multiple imports." },
|
||||
{ command: "decision diary edit|upsert <YYYY-MM-DD|id> --body-file path [--title text] [--source-file path] [--tag tag]", description: "Create or edit one daily diary entry through PUT /api/diary/entries/:idOrDate via backend-core proxy." },
|
||||
{ command: "decision list [--type ...] [--status ...] [--level|--priority ...] [--source text] [--issue id] [--linked-goal-id id] [--limit N]", description: "List Decision Center records through the user-service proxy." },
|
||||
{ command: "decision list [--type ...] [--status ...] [--level|--priority ...] [--source text] [--issue id] [--linked-goal-id id] [--limit N] [--include-body]", description: "List Decision Center records through the user-service proxy; bodies are omitted unless --include-body is set." },
|
||||
{ command: "decision requirement list|create|show|update|upsert [id] [--title text] [--body-file path] [--type external_goal|internal_goal|goal|decision|blocker|debt|experiment] [--source text] [--issue id]", description: "Manage productized requirement records over the PostgreSQL records model, excluding meeting records." },
|
||||
{ command: "decision show <id>", description: "Show one Decision Center record." },
|
||||
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from origin/master:deploy.json environments; --commit overrides one reviewed artifact consumer such as frontend for release/v1 validation or rollback. code-queue artifact consumption is dev-only." },
|
||||
|
||||
Reference in New Issue
Block a user