115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
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')",
|
|
"doc_no",
|
|
"doc_type",
|
|
"doc_priority",
|
|
"doc_year",
|
|
"doc_seq",
|
|
"signer",
|
|
"issued_at",
|
|
"effective_scope",
|
|
"supersedes",
|
|
"superseded_by",
|
|
]),
|
|
"requirements list route must stay on the records model, exclude meetings, and expose document fields",
|
|
);
|
|
|
|
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}` : \"\"}`",
|
|
"parseDocumentNo(optionValue(args, [\"--doc-no\", \"--docNo\", \"--document-no\", \"--documentNo\"])",
|
|
"params.set(\"docNo\", docNo)",
|
|
"payload.docType = docType",
|
|
"payload.signer = signer",
|
|
]),
|
|
"CLI must expose bounded list opt-in, diary source disambiguation, and document fields",
|
|
);
|
|
|
|
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-diary-source-and-document-query",
|
|
"frontend-exact-diary-row-and-record-edit-body",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runDecisionCenterQueryContract(), null, 2)}\n`);
|
|
}
|