feat(task-012): orchestrate parallel section production jobs
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ArticleDetailResponse, WorkflowEventSummary } from "@pipeline/shared";
|
||||
import type { AgentJobSummary, ArticleDetailResponse, WorkflowEventSummary } from "@pipeline/shared";
|
||||
|
||||
export type ArticleTimelineItem = Pick<
|
||||
WorkflowEventSummary,
|
||||
@@ -15,6 +15,19 @@ export type DetailSummary = {
|
||||
targetSite: string;
|
||||
updatedAt: string;
|
||||
timeline: ArticleTimelineItem[];
|
||||
productionArtifacts: ProductionArtifactRow[];
|
||||
};
|
||||
|
||||
export type ProductionArtifactRow = {
|
||||
jobId: string;
|
||||
artifactKey: string;
|
||||
artifactLabel: string;
|
||||
status: string;
|
||||
attempt: number;
|
||||
jobType: string;
|
||||
usedEvidenceIds: string[];
|
||||
unsupportedClaims: string[];
|
||||
errorMessage: string | null;
|
||||
};
|
||||
|
||||
export function buildArticleTimeline(detail: ArticleDetailResponse): ArticleTimelineItem[] {
|
||||
@@ -38,5 +51,64 @@ export function buildDetailSummary(detail: ArticleDetailResponse): DetailSummary
|
||||
targetSite: detail.target_site?.name ?? "Unknown",
|
||||
updatedAt: detail.article.updated_at,
|
||||
timeline: buildArticleTimeline(detail),
|
||||
productionArtifacts: buildProductionArtifactRows(detail.agent_jobs ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildProductionArtifactRows(
|
||||
jobs: readonly AgentJobSummary[],
|
||||
): ProductionArtifactRow[] {
|
||||
return jobs
|
||||
.filter((job) => job.job_type === "SECTION_SCAFFOLD")
|
||||
.map((job) => {
|
||||
const payload = (job.payload ?? {}) as Record<string, unknown>;
|
||||
const artifactKey = stringValue(payload.artifact_key) || `job:${job.id}`;
|
||||
const artifactLabel =
|
||||
stringValue(payload.artifact_label)
|
||||
|| stringValue(payload.heading)
|
||||
|| artifactKey;
|
||||
return {
|
||||
jobId: job.id,
|
||||
artifactKey,
|
||||
artifactLabel,
|
||||
status: job.status,
|
||||
attempt: job.attempt ?? 1,
|
||||
jobType: job.job_type,
|
||||
usedEvidenceIds: stringArray(payload.used_evidence_ids),
|
||||
unsupportedClaims: unsupportedClaims(payload.unsupported_claims),
|
||||
errorMessage: job.error_message ?? null,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.artifactLabel.localeCompare(right.artifactLabel));
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function stringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
|
||||
function unsupportedClaims(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
const claims: string[] = [];
|
||||
for (const item of value) {
|
||||
if (typeof item === "string") {
|
||||
claims.push(item);
|
||||
continue;
|
||||
}
|
||||
if (typeof item === "object" && item !== null && "claim_text" in item) {
|
||||
const claimText = item.claim_text;
|
||||
if (typeof claimText === "string") {
|
||||
claims.push(claimText);
|
||||
}
|
||||
}
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type DetailShellProps = {
|
||||
|
||||
export function ArticleDetailShell({ summary }: DetailShellProps) {
|
||||
const timeline = summary.timeline;
|
||||
const productionArtifacts = summary.productionArtifacts;
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -66,6 +67,46 @@ export function ArticleDetailShell({ summary }: DetailShellProps) {
|
||||
))}
|
||||
{timeline.length === 0 ? <li>No workflow events yet.</li> : null}
|
||||
</ul>
|
||||
|
||||
<h3>Production artifacts</h3>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artifact</th>
|
||||
<th>Status</th>
|
||||
<th>Attempt</th>
|
||||
<th>Used evidence IDs</th>
|
||||
<th>Unsupported claims</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{productionArtifacts.map((artifact) => (
|
||||
<tr key={artifact.jobId}>
|
||||
<td>
|
||||
<div>{artifact.artifactLabel}</div>
|
||||
<div>{artifact.artifactKey}</div>
|
||||
</td>
|
||||
<td>{artifact.status}</td>
|
||||
<td>{artifact.attempt}</td>
|
||||
<td>
|
||||
{artifact.usedEvidenceIds.length
|
||||
? artifact.usedEvidenceIds.join(", ")
|
||||
: "—"}
|
||||
</td>
|
||||
<td>
|
||||
{artifact.unsupportedClaims.length
|
||||
? artifact.unsupportedClaims.join("; ")
|
||||
: artifact.errorMessage ?? "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{productionArtifacts.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5}>No production artifacts yet.</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import ts from "typescript";
|
||||
|
||||
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const sourcePath = path.resolve(testDir, "../src/features/article-detail/model.ts");
|
||||
const source = readFileSync(sourcePath, "utf8");
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
});
|
||||
|
||||
const moduleExports = {};
|
||||
new Function("exports", compiled.outputText)(moduleExports);
|
||||
|
||||
const { buildProductionArtifactRows } = moduleExports;
|
||||
|
||||
const rows = buildProductionArtifactRows([
|
||||
{
|
||||
id: "job-section-1",
|
||||
article_id: "a1",
|
||||
parent_job_id: null,
|
||||
attempt: 1,
|
||||
job_type: "SECTION_SCAFFOLD",
|
||||
agent_profile: "fake-section-scaffold",
|
||||
status: "SUCCEEDED",
|
||||
workspace_path: null,
|
||||
input_files: [],
|
||||
output_files: [],
|
||||
payload: {
|
||||
artifact_key: "section:1",
|
||||
artifact_label: "Context and scope",
|
||||
used_evidence_ids: ["e1", "e2"],
|
||||
unsupported_claims: [],
|
||||
},
|
||||
error_category: null,
|
||||
error_message: null,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exit_code: 0,
|
||||
duration_ms: 3,
|
||||
queued_at: "2026-05-21T00:00:00Z",
|
||||
started_at: "2026-05-21T00:00:01Z",
|
||||
finished_at: "2026-05-21T00:00:02Z",
|
||||
},
|
||||
{
|
||||
id: "job-section-2",
|
||||
article_id: "a1",
|
||||
parent_job_id: null,
|
||||
attempt: 1,
|
||||
job_type: "SECTION_SCAFFOLD",
|
||||
agent_profile: "fake-section-scaffold",
|
||||
status: "FAILED",
|
||||
workspace_path: null,
|
||||
input_files: [],
|
||||
output_files: [],
|
||||
payload: {
|
||||
artifact_key: "section:2",
|
||||
artifact_label: "Implementation workflow",
|
||||
used_evidence_ids: ["e2"],
|
||||
unsupported_claims: [{ claim_text: "Unverified claim" }],
|
||||
},
|
||||
error_category: "UNSUPPORTED_CLAIMS_FOUND",
|
||||
error_message: "Unsupported claims introduced during scaffolding: 1",
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exit_code: 0,
|
||||
duration_ms: 3,
|
||||
queued_at: "2026-05-21T00:00:00Z",
|
||||
started_at: "2026-05-21T00:00:01Z",
|
||||
finished_at: "2026-05-21T00:00:02Z",
|
||||
},
|
||||
{
|
||||
id: "job-non-artifact",
|
||||
article_id: "a1",
|
||||
parent_job_id: null,
|
||||
attempt: 1,
|
||||
job_type: "TEST_CODEX",
|
||||
agent_profile: "fake-codex",
|
||||
status: "SUCCEEDED",
|
||||
workspace_path: null,
|
||||
input_files: [],
|
||||
output_files: [],
|
||||
payload: {},
|
||||
error_category: null,
|
||||
error_message: null,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exit_code: 0,
|
||||
duration_ms: 0,
|
||||
queued_at: "2026-05-21T00:00:00Z",
|
||||
started_at: "2026-05-21T00:00:01Z",
|
||||
finished_at: "2026-05-21T00:00:02Z",
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].artifactLabel, "Context and scope");
|
||||
assert.equal(rows[0].status, "SUCCEEDED");
|
||||
assert.deepEqual(rows[0].usedEvidenceIds, ["e1", "e2"]);
|
||||
assert.equal(rows[1].status, "FAILED");
|
||||
assert.deepEqual(rows[1].unsupportedClaims, ["Unverified claim"]);
|
||||
Reference in New Issue
Block a user