Merge pull request #267 from pikasTech/fix/issue191-decision-diary-summary
fix: preserve decision diary summary
This commit is contained in:
+2
-2
@@ -81,7 +81,7 @@
|
||||
{
|
||||
"id": "decision-center",
|
||||
"repo": "https://github.com/pikasTech/unidesk",
|
||||
"commitId": "b5486a61ab0aa6c227366a95d1afa68281584359"
|
||||
"commitId": "3ca82e9946ac4bc4a7e059df79c01e21407efb6f"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
{
|
||||
"id": "decision-center",
|
||||
"repo": "https://github.com/pikasTech/unidesk",
|
||||
"commitId": "b5486a61ab0aa6c227366a95d1afa68281584359",
|
||||
"commitId": "3ca82e9946ac4bc4a7e059df79c01e21407efb6f",
|
||||
"artifact": {
|
||||
"kind": "source-build",
|
||||
"repository": "unidesk/decision-center",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
const artifactRegistrySource = readFileSync("scripts/src/artifact-registry.ts", "utf8");
|
||||
const deploySource = readFileSync("scripts/src/deploy.ts", "utf8");
|
||||
|
||||
assertCondition(
|
||||
artifactRegistrySource.includes('function isLocalProvider(providerId: string): boolean'),
|
||||
"artifact-registry must define an explicit local provider predicate",
|
||||
);
|
||||
assertCondition(
|
||||
artifactRegistrySource.includes('providerId === "local"') && artifactRegistrySource.includes('providerId === "D601-local"'),
|
||||
"local provider predicate must accept only explicit local aliases",
|
||||
);
|
||||
assertCondition(
|
||||
artifactRegistrySource.includes('runCommand(["bash", "-lc", script], repoRoot, { timeoutMs })'),
|
||||
"local provider must execute the generated artifact script directly with bash -lc",
|
||||
);
|
||||
assertCondition(
|
||||
artifactRegistrySource.includes('local bash -lc <artifact-registry-${action}-readonly-script>'),
|
||||
"readonly command shape must disclose local execution instead of host.ssh",
|
||||
);
|
||||
assertCondition(
|
||||
deploySource.includes('providerId: string;'),
|
||||
"deploy options must carry providerId for artifact consumers",
|
||||
);
|
||||
assertCondition(
|
||||
deploySource.includes('providerId: optionValue(args, ["--provider-id", "--provider"]) ?? "D601"'),
|
||||
"deploy apply must parse provider-id with D601 as the default",
|
||||
);
|
||||
assertCondition(
|
||||
deploySource.includes('"--provider-id", options.providerId'),
|
||||
"deploy apply must forward provider-id to artifact-registry deploy-service",
|
||||
);
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"artifact-registry supports an explicit local/D601-local provider for D601 host CLI execution",
|
||||
"local provider runs the same generated scripts through bash -lc without provider SSH self-dispatch",
|
||||
"deploy apply forwards --provider-id to artifact-registry consumers while defaulting to D601",
|
||||
],
|
||||
}, null, 2)}\n`);
|
||||
@@ -4,7 +4,7 @@ import { rootPath } from "./src/config";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
const verifiedDecisionCenterCommit = "b5486a61ab0aa6c227366a95d1afa68281584359";
|
||||
const verifiedDecisionCenterCommit = "3ca82e9946ac4bc4a7e059df79c01e21407efb6f";
|
||||
const verifiedFrontendCommit = "7b9aa4261c216586954cdf926ce2c914a9db5ae3";
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { runDecisionCenterCommandAsync } from "./src/decision-center";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
interface FetchCall {
|
||||
path: string;
|
||||
init?: { method?: string; body?: 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 asRecord(value: unknown): JsonRecord {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as JsonRecord : {};
|
||||
}
|
||||
|
||||
function includesAll(text: string, snippets: string[]): boolean {
|
||||
return snippets.every((snippet) => text.includes(snippet));
|
||||
}
|
||||
|
||||
function makeFetcher(calls: FetchCall[]) {
|
||||
return async (path: string, init?: { method?: string; body?: unknown }): Promise<unknown> => {
|
||||
calls.push({ path, init });
|
||||
return { ok: true, status: 200, body: { ok: true, action: "updated", entry: { id: "diary_issue_191" } } };
|
||||
};
|
||||
}
|
||||
|
||||
async function assertCliPayloadContract(): Promise<string[]> {
|
||||
const checks: string[] = [];
|
||||
const config = {} as Parameters<typeof runDecisionCenterCommandAsync>[0];
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "unidesk-diary-summary-"));
|
||||
try {
|
||||
const bodyFile = join(tempDir, "body.md");
|
||||
const bodyText = "# 2099-12-31\n\n## 进展\n- body from file\n";
|
||||
writeFileSync(bodyFile, bodyText, "utf8");
|
||||
|
||||
{
|
||||
const calls: FetchCall[] = [];
|
||||
const result = asRecord(await runDecisionCenterCommandAsync(config, [
|
||||
"diary",
|
||||
"upsert",
|
||||
"2099-12-31",
|
||||
"--title",
|
||||
"Issue 191 Repro",
|
||||
"--summary",
|
||||
"explicit summary stays separate",
|
||||
"--body-file",
|
||||
bodyFile,
|
||||
"--source-file",
|
||||
"issue-191-contract",
|
||||
], makeFetcher(calls)));
|
||||
const call = calls[0];
|
||||
const body = asRecord(call?.init?.body);
|
||||
const bodySource = asRecord(result.bodySource);
|
||||
assertCondition(call?.path === "/api/microservices/decision-center/proxy/api/diary/entries/2099-12-31", "diary upsert must address date route", { call });
|
||||
assertCondition(call?.init?.method === "PUT", "diary upsert must use PUT", { call });
|
||||
assertCondition(body.summary === "explicit summary stays separate", "CLI must send explicit summary as its own payload field", { body });
|
||||
assertCondition(body.body === bodyText, "CLI must keep body populated from --body-file", { body });
|
||||
assertCondition(body.sourceFile === "issue-191-contract", "CLI must keep sourceFile disambiguation", { body });
|
||||
assertCondition(bodySource.kind === "file" && bodySource.path === bodyFile, "CLI must disclose file body source", { bodySource });
|
||||
checks.push("cli-diary-upsert-summary-plus-body-file-payload");
|
||||
}
|
||||
|
||||
{
|
||||
const calls: FetchCall[] = [];
|
||||
const result = asRecord(await runDecisionCenterCommandAsync(config, [
|
||||
"diary",
|
||||
"upsert",
|
||||
"2099-12-31",
|
||||
"--summary",
|
||||
"summary only update",
|
||||
], makeFetcher(calls)));
|
||||
const body = asRecord(calls[0]?.init?.body);
|
||||
const bodySource = asRecord(result.bodySource);
|
||||
assertCondition(body.summary === "summary only update", "CLI must support summary-only diary updates", { body });
|
||||
assertCondition(!("body" in body), "summary-only updates must not synthesize body from summary", { body });
|
||||
assertCondition(bodySource.kind === "none", "summary-only updates must disclose no body source", { bodySource });
|
||||
checks.push("cli-diary-upsert-summary-only-payload");
|
||||
}
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
export async function runDecisionCenterDiarySummaryContract(): Promise<JsonRecord> {
|
||||
const service = source("src/components/microservices/decision-center/src/index.ts");
|
||||
const cli = source("scripts/src/decision-center.ts");
|
||||
|
||||
assertCondition(
|
||||
includesAll(cli, [
|
||||
"function optionalBodyFromArgs(args: string[], command: string)",
|
||||
"const summary = optionValue(args, [\"--summary\"])",
|
||||
"if (summary !== undefined) payload.summary = summary",
|
||||
"bodySource: { kind: \"none\" }",
|
||||
]),
|
||||
"CLI must route --summary independently from body input",
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
includesAll(service, [
|
||||
"summary TEXT NOT NULL DEFAULT ''",
|
||||
"ALTER TABLE decision_center_diary_entries ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT ''",
|
||||
"summary: row.summary || summaryFromBody(body)",
|
||||
"function normalizeDiarySummary(value: unknown, body: string): string",
|
||||
"const summaryProvided = \"summary\" in input",
|
||||
"normalizeDiarySummary(input.summary, body)",
|
||||
"id, entry_date, month, title, summary, body, source_file",
|
||||
"summary = ${summary}",
|
||||
"OR summary IS DISTINCT FROM ${summary}",
|
||||
]),
|
||||
"Decision Center backend must persist explicit diary summary separately from body",
|
||||
);
|
||||
|
||||
const cliChecks = await assertCliPayloadContract();
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"cli-summary-independent-field-contract",
|
||||
"backend-diary-summary-column-contract",
|
||||
...cliChecks,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
process.stdout.write(`${JSON.stringify(await runDecisionCenterDiarySummaryContract(), null, 2)}\n`);
|
||||
}
|
||||
@@ -1245,9 +1245,14 @@ function registryRecommendedAction(classification: ArtifactRegistryFailureClassi
|
||||
}
|
||||
|
||||
function readonlyRemoteCommandShape(action: "status" | "health", options: ArtifactRegistryOptions): string {
|
||||
if (isLocalProvider(options.providerId)) return `local bash -lc <artifact-registry-${action}-readonly-script> timeoutMs=${options.timeoutMs}`;
|
||||
return `host.ssh provider=${options.providerId} mode=exec argv=bash -lc <artifact-registry-${action}-readonly-script> timeoutMs=${options.timeoutMs}`;
|
||||
}
|
||||
|
||||
function isLocalProvider(providerId: string): boolean {
|
||||
return providerId === "local" || providerId === "D601-local";
|
||||
}
|
||||
|
||||
function classifyProviderSshCommandFailure(command: CommandResult): ArtifactRegistryFailureClassification {
|
||||
const output = `${command.stderr}\n${command.stdout}`.toLowerCase();
|
||||
if (command.timedOut || output.includes("timed out") || output.includes("timeout")) return "remote-command-timeout";
|
||||
@@ -1652,8 +1657,11 @@ function artifactRegistryDeployJsonMirrors(
|
||||
}
|
||||
|
||||
function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs, runtime: ArtifactRegistryCommandRuntime = {}): CommandResult {
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
if (runtime.runRemoteScriptForTest !== undefined) return runtime.runRemoteScriptForTest(options, script, timeoutMs);
|
||||
if (isLocalProvider(options.providerId)) {
|
||||
return runCommand(["bash", "-lc", script], repoRoot, { timeoutMs });
|
||||
}
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
return runCommand(command, repoRoot, { timeoutMs });
|
||||
}
|
||||
|
||||
|
||||
@@ -461,6 +461,8 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d
|
||||
fileItem("scripts/src/ci.ts"),
|
||||
fileItem("scripts/src/e2e.ts"),
|
||||
fileItem("scripts/deploy-artifact-matrix-contract-test.ts"),
|
||||
fileItem("scripts/artifact-registry-local-provider-contract-test.ts"),
|
||||
fileItem("scripts/decision-center-diary-summary-contract-test.ts"),
|
||||
fileItem("scripts/decision-center-desired-state-contract-test.ts"),
|
||||
fileItem("scripts/code-queue-prompt-observation-test.ts"),
|
||||
fileItem("scripts/check-command-progress-contract-test.ts"),
|
||||
@@ -523,6 +525,8 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d
|
||||
items.push(await commandItem("provider:runner-triage-contract", ["bun", "scripts/provider-runner-triage-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("ssh:argv-guidance-contract", ["bun", "scripts/ssh-argv-guidance-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 90_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("artifact-registry:local-provider-contract", ["bun", "scripts/artifact-registry-local-provider-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("decision-center:diary-summary-contract", ["bun", "scripts/decision-center-diary-summary-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("decision-center:desired-state-contract", ["bun", "scripts/decision-center-desired-state-contract-test.ts"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
items.push(await commandItem("code-queue:trace-gap-not-stale", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:trace-gap-not-stale"], 30_000, process.env, options.checkHeartbeatMs));
|
||||
@@ -572,6 +576,8 @@ export async function runChecks(config: UniDeskConfig, options: CheckOptions = d
|
||||
items.push(skippedItem("provider:runner-triage-contract", "Provider runner triage contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("ssh:argv-guidance-contract", "SSH argv guidance and failure hint contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("artifact-registry:local-provider-contract", "artifact registry local provider contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("decision-center:diary-summary-contract", "Decision Center diary summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("decision-center:desired-state-contract", "Decision Center desired-state drift contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
|
||||
@@ -151,7 +151,7 @@ function readMarkdownFile(path: string): { absolutePath: string; markdown: strin
|
||||
return { absolutePath, markdown };
|
||||
}
|
||||
|
||||
function bodyFromArgs(args: string[], command: string): { body: string; bodySource: Record<string, string> } {
|
||||
function optionalBodyFromArgs(args: string[], command: string): { body: string | undefined; bodySource: Record<string, string> } {
|
||||
const body = optionValue(args, ["--body"]);
|
||||
const bodyFile = optionValue(args, ["--body-file", "--markdown-file"]);
|
||||
const markdownFile = optionValue(args, ["--file"]);
|
||||
@@ -163,6 +163,12 @@ function bodyFromArgs(args: string[], command: string): { body: string; bodySour
|
||||
const { absolutePath, markdown } = readMarkdownFile(file);
|
||||
return { body: markdown, bodySource: { kind: "file", path: absolutePath } };
|
||||
}
|
||||
return { body: undefined, bodySource: { kind: "none" } };
|
||||
}
|
||||
|
||||
function bodyFromArgs(args: string[], command: string): { body: string; bodySource: Record<string, string> } {
|
||||
const { body, bodySource } = optionalBodyFromArgs(args, command);
|
||||
if (body !== undefined) return { body, bodySource };
|
||||
throw new Error(`${command} requires --body text or --body-file path`);
|
||||
}
|
||||
|
||||
@@ -407,14 +413,18 @@ async function todayDiaryAsync(fetcher: (path: string, init?: { method?: string;
|
||||
function diaryEditPayload(args: string[], command: string): { key: string; payload: Record<string, unknown>; bodySource: Record<string, string> } {
|
||||
const key = positionalArgs(args)[0];
|
||||
if (!key) throw new Error(`${command} requires entry id or YYYY-MM-DD date`);
|
||||
const { body, bodySource } = bodyFromArgs(args, command);
|
||||
const payload: Record<string, unknown> = { body };
|
||||
const { body, bodySource } = optionalBodyFromArgs(args, command);
|
||||
const payload: Record<string, unknown> = {};
|
||||
const title = optionValue(args, ["--title"]);
|
||||
const summary = optionValue(args, ["--summary"]);
|
||||
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
|
||||
if (body !== undefined) payload.body = body;
|
||||
if (title !== undefined) payload.title = title;
|
||||
if (summary !== undefined) payload.summary = summary;
|
||||
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
|
||||
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
|
||||
if (tags.length > 0) payload.tags = tags;
|
||||
if (Object.keys(payload).length === 0) throw new Error(`${command} requires --body text, --body-file path, --summary, --title, --source-file, or --tag`);
|
||||
return { key, payload, bodySource };
|
||||
}
|
||||
|
||||
@@ -429,26 +439,34 @@ async function editDiaryAsync(args: string[], fetcher: (path: string, init?: { m
|
||||
}
|
||||
|
||||
function editTodayDiary(args: string[]): unknown {
|
||||
const { body, bodySource } = bodyFromArgs(args, "decision diary today --edit");
|
||||
const payload: Record<string, unknown> = { body };
|
||||
const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit");
|
||||
const payload: Record<string, unknown> = {};
|
||||
const title = optionValue(args, ["--title"]);
|
||||
const summary = optionValue(args, ["--summary"]);
|
||||
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
|
||||
if (body !== undefined) payload.body = body;
|
||||
if (title !== undefined) payload.title = title;
|
||||
if (summary !== undefined) payload.summary = summary;
|
||||
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
|
||||
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
|
||||
if (tags.length > 0) payload.tags = tags;
|
||||
if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag");
|
||||
return { bodySource, result: unwrapProxyResponse(decisionProxy("/api/diary/today", { method: "PUT", body: payload })) };
|
||||
}
|
||||
|
||||
async function editTodayDiaryAsync(args: string[], fetcher: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>): Promise<unknown> {
|
||||
const { body, bodySource } = bodyFromArgs(args, "decision diary today --edit");
|
||||
const payload: Record<string, unknown> = { body };
|
||||
const { body, bodySource } = optionalBodyFromArgs(args, "decision diary today --edit");
|
||||
const payload: Record<string, unknown> = {};
|
||||
const title = optionValue(args, ["--title"]);
|
||||
const summary = optionValue(args, ["--summary"]);
|
||||
const sourceFile = optionValue(args, ["--source-file", "--source-path", "--source"]);
|
||||
if (body !== undefined) payload.body = body;
|
||||
if (title !== undefined) payload.title = title;
|
||||
if (summary !== undefined) payload.summary = summary;
|
||||
if (sourceFile !== undefined) payload.sourceFile = sourceFile;
|
||||
const tags = splitList(optionValues(args, ["--tag", "--tags"]));
|
||||
if (tags.length > 0) payload.tags = tags;
|
||||
if (Object.keys(payload).length === 0) throw new Error("decision diary today --edit requires --body text, --body-file path, --summary, --title, --source-file, or --tag");
|
||||
return { bodySource, result: unwrapProxyResponse(await decisionProxyAsync(fetcher, "/api/diary/today", { method: "PUT", body: payload })) };
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ interface DeployOptions {
|
||||
environment: DeployEnvironment | null;
|
||||
serviceId: string | null;
|
||||
commitOverride: string | null;
|
||||
providerId: string;
|
||||
runNow: boolean;
|
||||
dryRun: boolean;
|
||||
force: boolean;
|
||||
@@ -221,7 +222,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
|
||||
usage: {
|
||||
check: "bun scripts/cli.ts deploy check [--file deploy.json | --env dev|prod] [--service id]",
|
||||
plan: "bun scripts/cli.ts deploy plan [--file deploy.json | --env dev|prod] [--service id]",
|
||||
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
|
||||
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force] [--timeout-ms N] [--provider-id D601|local] [--run-now]",
|
||||
guard: "bun scripts/cli.ts deploy guard code-queue-source [--root /home/ubuntu/cq-deploy]",
|
||||
},
|
||||
actions: {
|
||||
@@ -244,6 +245,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
|
||||
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
|
||||
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
|
||||
{ name: "--timeout-ms <n>", default: defaultTimeoutMs, description: "Per-step timeout budget where supported." },
|
||||
{ name: "--provider-id <id>", default: "D601", description: "Provider used by artifact-registry consumers; use local only from the D601 host CLI." },
|
||||
{ name: "--run-now", description: "Run apply in the foreground worker process; omit it for fire-and-forget async job mode." },
|
||||
{ name: "guard code-queue-source --root <path>", description: "Validate Code Queue hostPath source relative imports before any scheduler rollout; failures report degradedReason and missing import targets." },
|
||||
],
|
||||
@@ -523,6 +525,7 @@ function parseOptions(args: string[]): DeployOptions {
|
||||
environment,
|
||||
serviceId,
|
||||
commitOverride: commitOverride?.toLowerCase() ?? null,
|
||||
providerId: optionValue(args, ["--provider-id", "--provider"]) ?? "D601",
|
||||
runNow: args.includes("--run-now"),
|
||||
dryRun: args.includes("--dry-run"),
|
||||
force: args.includes("--force"),
|
||||
@@ -2882,6 +2885,7 @@ async function runDevArtifactConsumerService(
|
||||
"--commit", commit,
|
||||
"--source-repo", desired.repo,
|
||||
"--timeout-ms", String(options.timeoutMs),
|
||||
"--provider-id", options.providerId,
|
||||
"--run-now",
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
];
|
||||
@@ -3404,6 +3408,7 @@ async function runArtifactConsumerApplyNow(
|
||||
...(options.dryRun && hasDeployJsonExecutorContract(service) ? ["--deploy-json-service", encodeDeployJsonServiceContract(service)] : []),
|
||||
"--env", environment,
|
||||
"--timeout-ms", String(options.timeoutMs),
|
||||
"--provider-id", options.providerId,
|
||||
"--run-now",
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
];
|
||||
|
||||
+2
-2
@@ -42,10 +42,10 @@ export function rootHelp(): unknown {
|
||||
{ command: "decision diary import <markdown-file> [--source-file path] [--tag tag] [--include-entries]", description: "Import a dated work log Markdown into PostgreSQL diary entries split as YYYY-MM/YYYY-MM-DD.md." },
|
||||
{ command: "decision diary list [--month YYYY-MM] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--limit N] [--include-body]", description: "List daily Markdown diary entries stored by Decision Center." },
|
||||
{ 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 today [--edit --body-file path|--body text|--summary text] [--title text] [--tag tag]", description: "Get or create today's diary entry using the service's real current date; --edit saves today's Markdown and optional explicit summary." },
|
||||
{ command: "decision diary months", description: "List available Decision Center diary months with day counts." },
|
||||
{ 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 diary edit|upsert <YYYY-MM-DD|id> [--body-file path|--body text] [--summary text] [--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, keeping summary independent from body." },
|
||||
{ command: "decision list [--doc-no DC-...] [--doc-type ...] [--doc-priority P0|P1|P2|P3] [--year YYYY] [--type ...] [--status ...] [--level|--priority ...] [--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|docNo] [--title text] [--body-file path] [--type external_goal|internal_goal|goal|decision|blocker|debt|experiment] [--doc-no DC-...] [--doc-type ...] [--doc-priority P0|P1|P2|P3] [--signer text] [--issued-at ISO]", description: "Manage productized requirement records over the PostgreSQL records model, excluding meeting records." },
|
||||
{ command: "decision show <id|docNo>", description: "Show one Decision Center record." },
|
||||
|
||||
@@ -102,6 +102,7 @@ interface DiaryEntryRow {
|
||||
entry_date: Date | string;
|
||||
month: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
body: string;
|
||||
source_file: string;
|
||||
markdown_path: string;
|
||||
@@ -714,6 +715,7 @@ async function ensureSchema(): Promise<void> {
|
||||
entry_date DATE NOT NULL,
|
||||
month TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
source_file TEXT NOT NULL DEFAULT '',
|
||||
markdown_path TEXT NOT NULL,
|
||||
@@ -727,6 +729,7 @@ async function ensureSchema(): Promise<void> {
|
||||
CONSTRAINT decision_center_diary_entries_source_date_unique UNIQUE (source_file, entry_date)
|
||||
)
|
||||
`;
|
||||
await sql`ALTER TABLE decision_center_diary_entries ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT ''`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_decision_center_diary_entries_month_date ON decision_center_diary_entries(month DESC, entry_date DESC)`;
|
||||
await sql`CREATE INDEX IF NOT EXISTS idx_decision_center_diary_entries_updated ON decision_center_diary_entries(updated_at DESC)`;
|
||||
}, { retryRead: true });
|
||||
@@ -1310,7 +1313,7 @@ function diaryEntryFromRow(row: DiaryEntryRow, options: { includeBody?: boolean
|
||||
date: dateOnly(row.entry_date),
|
||||
month: row.month,
|
||||
title: row.title,
|
||||
summary: summaryFromBody(body),
|
||||
summary: row.summary || summaryFromBody(body),
|
||||
body: includeBody ? body : "",
|
||||
sourceFile: row.source_file,
|
||||
markdownPath: row.markdown_path,
|
||||
@@ -1322,6 +1325,11 @@ function diaryEntryFromRow(row: DiaryEntryRow, options: { includeBody?: boolean
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDiarySummary(value: unknown, body: string): string {
|
||||
const explicit = asText(value).replace(/\s+/gu, " ").trim();
|
||||
return (explicit || summaryFromBody(body)).slice(0, 280);
|
||||
}
|
||||
|
||||
function splitDiaryMarkdown(markdown: string): DiaryDraft[] {
|
||||
const normalized = markdown.replace(/\r\n?/gu, "\n");
|
||||
const lines = normalized.split("\n");
|
||||
@@ -1410,20 +1418,22 @@ function diaryTitleFor(date: string, body: string, rawTitle: unknown): string {
|
||||
async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: string[]): Promise<{ entry: DiaryEntry; action: "created" | "updated" | "unchanged" }> {
|
||||
if (draft.body.length > 300_000) throw new HttpError(400, "diary day body must be at most 300000 characters", { date: draft.date, length: draft.body.length });
|
||||
const hash = contentHash(draft.body);
|
||||
const summary = summaryFromBody(draft.body);
|
||||
const rows = await withDatabaseRecovery("upsert_diary_entry", () => sql<DiaryUpsertRow[]>`
|
||||
WITH existing AS (
|
||||
SELECT content_hash
|
||||
SELECT content_hash, summary
|
||||
FROM decision_center_diary_entries
|
||||
WHERE source_file = ${sourceFile} AND entry_date = ${draft.date}::date
|
||||
),
|
||||
upsert AS (
|
||||
INSERT INTO decision_center_diary_entries (
|
||||
id, entry_date, month, title, body, source_file, markdown_path, tags, content_hash
|
||||
id, entry_date, month, title, summary, body, source_file, markdown_path, tags, content_hash
|
||||
) VALUES (
|
||||
${`diary_${randomUUID()}`},
|
||||
${draft.date}::date,
|
||||
${draft.month},
|
||||
${draft.title},
|
||||
${summary},
|
||||
${draft.body},
|
||||
${sourceFile},
|
||||
${draft.markdownPath},
|
||||
@@ -1434,13 +1444,16 @@ async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: str
|
||||
SET
|
||||
month = EXCLUDED.month,
|
||||
title = EXCLUDED.title,
|
||||
summary = EXCLUDED.summary,
|
||||
body = EXCLUDED.body,
|
||||
markdown_path = EXCLUDED.markdown_path,
|
||||
tags = EXCLUDED.tags,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
imported_at = now(),
|
||||
updated_at = CASE
|
||||
WHEN decision_center_diary_entries.content_hash IS DISTINCT FROM EXCLUDED.content_hash THEN now()
|
||||
WHEN decision_center_diary_entries.content_hash IS DISTINCT FROM EXCLUDED.content_hash
|
||||
OR decision_center_diary_entries.summary IS DISTINCT FROM EXCLUDED.summary
|
||||
THEN now()
|
||||
ELSE decision_center_diary_entries.updated_at
|
||||
END
|
||||
RETURNING *
|
||||
@@ -1448,7 +1461,7 @@ async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: str
|
||||
SELECT
|
||||
upsert.*,
|
||||
NOT EXISTS(SELECT 1 FROM existing) AS inserted,
|
||||
EXISTS(SELECT 1 FROM existing WHERE existing.content_hash IS DISTINCT FROM upsert.content_hash) AS changed
|
||||
EXISTS(SELECT 1 FROM existing WHERE existing.content_hash IS DISTINCT FROM upsert.content_hash OR existing.summary IS DISTINCT FROM upsert.summary) AS changed
|
||||
FROM upsert
|
||||
`);
|
||||
const row = rows[0];
|
||||
@@ -1513,6 +1526,7 @@ async function listDiaryEntries(url: URL): Promise<DiaryEntry[]> {
|
||||
entry_date,
|
||||
month,
|
||||
title,
|
||||
summary,
|
||||
CASE WHEN ${includeBody}::boolean THEN body ELSE left(body, 4000) END AS body,
|
||||
source_file,
|
||||
markdown_path,
|
||||
@@ -1569,6 +1583,7 @@ async function getDiaryEntry(key: string, options: { sourceFile?: string } = {})
|
||||
async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>): Promise<JsonRecord> {
|
||||
const keyDate = /^\d{4}-\d{2}-\d{2}$/u.test(key) ? validDateFilter(key, "date") : "";
|
||||
const bodyProvided = "body" in input || "markdown" in input;
|
||||
const summaryProvided = "summary" in input;
|
||||
const titleProvided = "title" in input;
|
||||
const tagsProvided = "tags" in input;
|
||||
const sourceFileProvided = "sourceFile" in input || "sourcePath" in input || "source" in input;
|
||||
@@ -1592,6 +1607,11 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
|
||||
const date = existing === undefined ? keyDate : dateOnly(existing.entry_date);
|
||||
const body = bodyProvided ? asText(input.body ?? input.markdown) : existing?.body ?? defaultDiaryBody(date);
|
||||
if (body.length > 300_000) throw new HttpError(400, "diary day body must be at most 300000 characters", { date, length: body.length });
|
||||
const summary = summaryProvided
|
||||
? normalizeDiarySummary(input.summary, body)
|
||||
: bodyProvided || existing === undefined
|
||||
? summaryFromBody(body)
|
||||
: existing.summary || summaryFromBody(body);
|
||||
const title = titleProvided || existing === undefined ? diaryTitleFor(date, body, input.title) : existing.title;
|
||||
const tags = tagsProvided ? asStringArray(input.tags, "tags") : Array.isArray(existing?.tags) ? existing.tags.map(String) : [];
|
||||
const finalSourceFile = sourceFileProvided ? sourceFile : validateDiarySourceFile(existing?.source_file ?? "manual");
|
||||
@@ -1602,12 +1622,13 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
|
||||
const rows = existing === undefined
|
||||
? await withDatabaseRecovery("create_diary_entry_by_date", () => sql<DiaryEntryRow[]>`
|
||||
INSERT INTO decision_center_diary_entries (
|
||||
id, entry_date, month, title, body, source_file, markdown_path, tags, content_hash, imported_at, updated_at
|
||||
id, entry_date, month, title, summary, body, source_file, markdown_path, tags, content_hash, imported_at, updated_at
|
||||
) VALUES (
|
||||
${`diary_${randomUUID()}`},
|
||||
${date}::date,
|
||||
${month},
|
||||
${title},
|
||||
${summary},
|
||||
${body},
|
||||
${finalSourceFile},
|
||||
${markdownPath},
|
||||
@@ -1622,6 +1643,7 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
|
||||
UPDATE decision_center_diary_entries
|
||||
SET
|
||||
title = ${title},
|
||||
summary = ${summary},
|
||||
body = ${body},
|
||||
source_file = ${finalSourceFile},
|
||||
markdown_path = ${markdownPath},
|
||||
@@ -1631,6 +1653,7 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
|
||||
updated_at = CASE
|
||||
WHEN content_hash IS DISTINCT FROM ${hash}
|
||||
OR title IS DISTINCT FROM ${title}
|
||||
OR summary IS DISTINCT FROM ${summary}
|
||||
OR source_file IS DISTINCT FROM ${finalSourceFile}
|
||||
OR tags IS DISTINCT FROM ${sql.json(tags)}::jsonb
|
||||
THEN now()
|
||||
|
||||
Reference in New Issue
Block a user