Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 18 additions & 1 deletion apps/presentation/dashboard/smoke/delivery-review-smoke.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { deliveryReviewMarkdown, filterReviewNodes, parseDeliveryReview, reviewCoverageIncomplete } from "../node_modules/.cache/delivery-review/data/delivery-review.js";
import { deliveryReviewMarkdown, deliveryReviewWarning, filterReviewNodes, parseDeliveryReview, reviewCoverageIncomplete } from "../node_modules/.cache/delivery-review/data/delivery-review.js";
import { deliveryReviewCopy } from "../node_modules/.cache/delivery-review/features/personal-workspace/delivery-review-copy.js";

const snapshot = {
Expand All @@ -23,6 +23,23 @@ const snapshot = {
},
};
const before = JSON.stringify(snapshot);
const envelope = {
schema_version: "loopx_projection_envelope_v0", projection: "delivery_review",
observed_at: snapshot.observed_at, served_at: snapshot.observed_at, fresh: true, complete: true,
alert: false, alert_reasons: [], sources: [{source_id: "goal_todos", last_read_at: snapshot.observed_at, status: "fresh"}],
coverage: {scope: "selected_goal_delivery", expected_count: 3, included_count: 3, omitted: []},
};
for (const copy of Object.values(deliveryReviewCopy)) {
assert.equal(deliveryReviewWarning(parseDeliveryReview(snapshot, "review-demo"), copy), copy.sourceUnknown);
const current = parseDeliveryReview({...snapshot, projection_envelope: envelope}, "review-demo");
assert.equal(deliveryReviewWarning(current, copy), null);
const stale = parseDeliveryReview({...snapshot, projection_envelope: {...envelope, fresh: false, alert: true}}, "review-demo");
assert.equal(deliveryReviewWarning(stale, copy), copy.sourceWarning);
assert.ok(deliveryReviewMarkdown(stale, copy).includes('"fresh": false'));
assert.equal(deliveryReviewWarning({...current, projection_envelope: {...envelope, complete: false}}, copy), copy.coverageWarning);
}
assert.throws(() => parseDeliveryReview({...snapshot, projection_envelope: {...envelope, projection: "status"}}, "review-demo"));
assert.throws(() => parseDeliveryReview({...snapshot, projection_envelope: {...envelope, fresh: "true"}}, "review-demo"));
const parsed = parseDeliveryReview(snapshot, "review-demo");
const graph = parsed.graph;
assert.equal(graph.edges.length, 4, "Parallel lineage and dependency edges must survive");
Expand Down
29 changes: 28 additions & 1 deletion apps/presentation/dashboard/src/data/delivery-review.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { z } from "zod";
import { goalAcceptanceDisplayStatus, goalAcceptanceObservationSchema, type GoalAcceptanceContract } from "./goal-acceptance-observation.js";

// Consumer validation only; freshness and coverage remain kernel decisions.
const projectionEnvelopeSchema = z.object({
schema_version: z.literal("loopx_projection_envelope_v0"), projection: z.string(),
observed_at: z.string().datetime({offset: true}), served_at: z.string().datetime({offset: true}),
fresh: z.boolean(), complete: z.boolean(), alert: z.boolean(), alert_reasons: z.array(z.string()),
sources: z.array(z.object({source_id: z.string(), last_read_at: z.string().nullable(), status: z.string()}).passthrough()),
coverage: z.object({scope: z.string(), expected_count: z.number().int().nonnegative().nullable(),
included_count: z.number().int().nonnegative(), omitted: z.array(z.object({reason: z.string(), count: z.number().int().positive()}).passthrough())}).passthrough(),
}).passthrough();

const refsSchema = z.record(z.string(), z.array(z.string()));
const nodeSchema = z.object({
node_id: z.string().min(1),
Expand Down Expand Up @@ -45,6 +55,7 @@ const goalMapNodeSchema = z.object({
});
const goalMapSchema = z.object({
schema_version: z.literal("goal_task_map_v0"), mode: z.literal("read_only"), goal_id: z.string(),
projection_envelope: projectionEnvelopeSchema.optional(),
limits: z.object({
node_limit: z.number().int().positive(), emitted_node_count: z.number().int().nonnegative(),
omitted_node_count: z.number().int().nonnegative(), source_truncated: z.boolean(),
Expand All @@ -66,6 +77,7 @@ const goalMapSchema = z.object({
});
const snapshotSchema = z.object({
ok: z.literal(true), goal_id: z.string(), observed_at: z.string().datetime({ offset: true }),
projection_envelope: projectionEnvelopeSchema.optional(),
graph: graphSchema.nullable(), goal_map: goalMapSchema.nullable().optional(),
acceptance: goalAcceptanceObservationSchema.nullable(),
});
Expand All @@ -81,7 +93,9 @@ export function parseDeliveryReview(value: unknown, goalId: string): DeliveryRev
const result = snapshotSchema.parse(value);
if (result.goal_id !== goalId || (result.graph && result.graph.goal_id !== goalId)
|| (result.goal_map && result.goal_map.goal_id !== goalId)
|| (result.acceptance && result.acceptance.goal_id !== goalId)) {
|| (result.acceptance && result.acceptance.goal_id !== goalId)
|| (result.projection_envelope && result.projection_envelope.projection !== "delivery_review")
|| (result.goal_map?.projection_envelope && result.goal_map.projection_envelope.projection !== "goal_task_map")) {
throw new Error("Review source does not match the selected Goal");
}
return result;
Expand Down Expand Up @@ -136,15 +150,27 @@ export type ReviewExportLabels = {
acceptance: string; acceptanceBoundary: string; noGraph: string; incomplete: string;
unavailable: string; refs: string; required: string; guards: string; next: string;
owner: string; reason: string; historical: string; checks: string; missingSources: string; observedScope: string;
sourceUnknown: string; sourceWarning: string; coverageWarning: string;
kind: Record<ReviewNode["kind"], string>; state: Record<ReviewNode["state"], string>;
relation: Record<ReviewRelation, string>;
workMap: { title: string; boundary: string; kind: Record<GoalMapNode["kind"], string>; relation: Record<GoalMapEdge["relation"], string> } & Record<string, unknown>;
};

/** Display the owner's flags, including unknown older responses; do not age a second clock. */
export function deliveryReviewWarning(snapshot: DeliveryReviewSnapshot, labels: Pick<ReviewExportLabels,
"sourceUnknown" | "sourceWarning" | "coverageWarning">) {
const envelope = snapshot.projection_envelope;
return !envelope ? labels.sourceUnknown : !envelope.fresh ? labels.sourceWarning
: !envelope.complete ? labels.coverageWarning : null;
}

/** Export the entire validated snapshot, never the search-filtered screen. */
export function deliveryReviewMarkdown(snapshot: DeliveryReviewSnapshot, labels: ReviewExportLabels) {
const line = (value: unknown) => String(value ?? labels.unavailable).replace(/[\\`*_{}[\]<>|#]/g, "\\$&").replace(/[\r\n]+/g, " ");
const rows = [`# ${labels.title}`, "", `Goal: ${line(snapshot.goal_id)}`, `${labels.observed}: ${line(snapshot.observed_at)}`, "", labels.scope, "", labels.acceptanceBoundary, "", `## ${labels.chain}`, ""];
const warning = deliveryReviewWarning(snapshot, labels);
if (warning) rows.push(warning, "");
if (snapshot.projection_envelope) rows.push("```json", JSON.stringify(snapshot.projection_envelope, null, 2), "```", "");
const graph = snapshot.graph;
if (!graph) rows.push(labels.noGraph);
else {
Expand All @@ -164,6 +190,7 @@ export function deliveryReviewMarkdown(snapshot: DeliveryReviewSnapshot, labels:
const map = snapshot.goal_map;
if (map) {
rows.push("", `## ${labels.workMap.title}`, "", labels.workMap.boundary, "", "```json", JSON.stringify(map.limits, null, 2), "```", "");
if (map.projection_envelope) rows.push("```json", JSON.stringify(map.projection_envelope, null, 2), "```", "");
const names = new Map(map.nodes.map(node => [node.node_id, node.title]));
for (const node of map.nodes) rows.push(`- ${line(node.title)} · ${labels.workMap.kind[node.kind]} · ${labels.state[node.state]}${node.owner_agent ? ` · ${line(node.owner_agent)}` : ""} (${line(node.refs.todo_ids?.join(", "))})`);
if (map.edges.length) rows.push("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const en = {
contract: goalAcceptanceContractCopy.en,
title: "Delivery & evidence", scope: "The delivery chain covers current work, a few predecessors and their evidence. The work map covers every task that is not archived.",
observed: "Snapshot read", chain: "Delivery chain", relations: "Relationships", acceptance: "Acceptance observations",
sourceUnknown: "Source freshness and coverage are unknown in this snapshot. Refresh to read them.",
sourceWarning: "Some sources are outdated or unreadable. Refresh before relying on this snapshot.",
coverageWarning: "This snapshot does not cover all requested information. Check the missing or unexpanded sources below.",
acceptanceBoundary: "Completed tasks and recorded evidence do not certify Goal acceptance.",
noGraph: "No delivery chain is available in this snapshot. This does not mean all work is complete.",
incomplete: "Some related information is missing or not expanded.",
Expand Down Expand Up @@ -59,7 +62,7 @@ const en = {
tasksDone: "tasks done", blocked: "blocked", waiting: "waiting", decisions: "need your decision", watches: "watching",
hidden: "finished or deferred, hidden", unlinked: "Not linked to other work", empty: "This Goal has no tasks yet.",
owner: "Owner", showMore: "Show {count} more", showFewer: "Show fewer", doneBefore: "+{count} done before", noLinks: "No task links are recorded between these items yet.",
unavailable: "This source does not provide a work map.",
unavailable: "The work map is unavailable. Refresh to retry.",
kind: { deliverable: "Task", gate: "Decision", monitor: "Watch" },
tone: { decision: "Needs you", blocked: "Blocked", open: "Open", waiting: "Waiting", done: "Done", unknown: "Unknown" },
before: "Before this", after: "After this", nothingBefore: "No recorded prerequisites.", nothingAfter: "Nothing recorded depends on this yet.",
Expand All @@ -74,6 +77,9 @@ const zh: typeof en = {
contract: goalAcceptanceContractCopy["zh-CN"],
title: "交付与依据", scope: "交付链仅含当前工作、少量前序及其证据;工作地图包含全部未归档任务。",
observed: "快照读取时间", chain: "交付链", relations: "关联关系", acceptance: "验收观察",
sourceUnknown: "当前快照的来源新鲜度与覆盖范围未知,请刷新重新读取。",
sourceWarning: "部分来源已过期或不可读,请刷新后再据此判断。",
coverageWarning: "当前快照未覆盖全部请求信息,请查看下方缺失或未展开的来源。",
acceptanceBoundary: "任务完成、已有证据均不等于 Goal 已通过验收。",
noGraph: "当前快照没有可展示的交付链,这不代表工作已经全部完成。",
incomplete: "部分关联信息缺失或未展开。",
Expand Down Expand Up @@ -101,7 +107,7 @@ const zh: typeof en = {
tasksDone: "项任务已完成", blocked: "受阻", waiting: "等待", decisions: "需你决策", watches: "持续监控",
hidden: "项已完成或延后的工作已隐藏", unlinked: "未与其他工作关联", empty: "此 Goal 还没有任务。",
owner: "负责人", showMore: "显示其余 {count} 项", showFewer: "收起", doneBefore: "另有 {count} 项已完成前序", noLinks: "这些事项之间还没有记录的任务关联。",
unavailable: "此来源不提供工作地图。",
unavailable: "工作地图暂不可用,请刷新重试。",
kind: { deliverable: "任务", gate: "决策", monitor: "监控" },
tone: { decision: "需要你", blocked: "受阻", open: "待处理", waiting: "等待", done: "已完成", unknown: "未知" },
before: "之前", after: "之后", nothingBefore: "未记录前置工作。", nothingAfter: "暂无记录依赖此项的工作。",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useId, useMemo, useRef, useState } from "react";
import { ArrowRight, CircleAlert, Download, ExternalLink, RefreshCw, Search } from "lucide-react";
import { deliveryReviewMarkdown, filterReviewNodes, reviewCoverageIncomplete, reviewNodeColumn, type ReviewFocus, type ReviewGraph, type ReviewNode } from "../../data/delivery-review";
import { deliveryReviewMarkdown, deliveryReviewWarning, filterReviewNodes, reviewCoverageIncomplete, reviewNodeColumn, type ReviewFocus, type ReviewGraph, type ReviewNode } from "../../data/delivery-review";
import type { WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceModel, WorkspaceTimelineItem } from "./personal-workspace-model";
import { GoalAcceptanceObservationCard } from "./goal-acceptance-observation-card";
import { GoalAcceptanceContractSection } from "./goal-acceptance-contract";
Expand All @@ -13,6 +13,14 @@ import "./delivery-review.css";

type Copy = typeof deliveryReviewCopy.en;

function SourceNotice({review, copy, mapOnly = false}: {review: DeliveryReviewRead; copy: Copy; mapOnly?: boolean}) {
const snapshot = review.snapshot;
const warning = snapshot ? deliveryReviewWarning(mapOnly ? {...snapshot,
projection_envelope: snapshot.goal_map?.projection_envelope} : snapshot, copy) : null;
const envelope = mapOnly ? snapshot?.goal_map?.projection_envelope : snapshot?.projection_envelope;
return warning ? <p role={!envelope || !envelope.fresh ? "alert" : "status"} className="delivery-notice"><CircleAlert aria-hidden size={15} />{warning}</p> : null;
}

function ReviewMap({ graph, nodes, selected, onSelect, copy }: {
graph: ReviewGraph; nodes: ReviewNode[]; selected: string | null; onSelect: (id: string) => void; copy: Copy;
}) {
Expand Down Expand Up @@ -119,7 +127,8 @@ export function DeliveryReviewContent({ goal, items, userTodos, onSelect, review
<p className="delivery-snapshot-time">{copy.observed} · <time dateTime={snapshot.observed_at}>{new Date(snapshot.observed_at).toLocaleString(locale)}</time>
{state.kind === "loading" || (stale && state.kind !== "error") ? <span role="status"> · {copy.updating}</span> : null}</p>
{stale && state.kind === "error" ? <p role="alert" className="delivery-notice is-error"><CircleAlert aria-hidden size={15} />{copy.changed}</p> : null}
{!hideWorkMap ? <DeliveryWorkMap goal={goal} userTodos={userTodos} review={review} onSelect={selection => {if (selection) onSelect(selection);}} /> : null}
<SourceNotice review={review} copy={copy} />
{!hideWorkMap ? <DeliveryWorkMap goal={goal} userTodos={userTodos} review={review} showSourceNotice={false} onSelect={selection => {if (selection) onSelect(selection);}} /> : null}
<p className="delivery-boundary">{copy.scope} {copy.acceptanceBoundary}</p>
{graph ? <>
{reviewCoverageIncomplete(graph) ? <details className="delivery-notice"><summary>{copy.incomplete}</summary><dl>
Expand Down Expand Up @@ -160,9 +169,10 @@ export function DeliveryReviewContent({ goal, items, userTodos, onSelect, review
}

/** Same map and exact-request fallback, now usable as an Overview work lens. */
export function DeliveryWorkMap({goal, userTodos, review, onSelect, selectedTodoId}: {
export function DeliveryWorkMap({goal, userTodos, review, onSelect, selectedTodoId, showSourceNotice = true}: {
goal: WorkspaceGoal; userTodos: WorkspaceModel["userTodos"]; review: DeliveryReviewRead;
onSelect: (selection: WorkspaceDrawerSelection | null) => void; selectedTodoId?: string | null;
showSourceNotice?: boolean;
}) {
const {locale} = useWorkspaceI18n();
const copy = deliveryReviewCopy[locale];
Expand All @@ -185,13 +195,17 @@ export function DeliveryWorkMap({goal, userTodos, review, onSelect, selectedTodo
return { kind: "todo", item: { ...todo, goalId: goal.goalId, goalTitle: goal.title, ownerLabel: todo.claimedBy } };
}
const map = review.snapshot?.goal_map;
if (!map) return <p role="status">{review.kind === "loading" ? copy.loading : copy.workMap.unavailable}</p>;
if (!map) return <div className="delivery-notice" role="status">
{review.kind === "loading" ? copy.loading : <>{copy.workMap.unavailable}
<button type="button" onClick={() => review.refresh()}><RefreshCw aria-hidden size={15} />{copy.refresh}</button></>}
</div>;
const selected = selectedTodoId === undefined ? localSelection
: selectedTodoId ? map.nodes.find(node => node.refs.todo_ids?.includes(selectedTodoId))?.node_id ?? null
: (() => {const node = map.nodes.find(node => node.node_id === localSelection);
return node && (node.kind === "gate" || !node.refs.todo_ids?.length) ? localSelection : null;}) ();
return <>
{!review.usable ? <p role="status">{review.kind === "error" ? copy.refreshError : copy.updating}</p> : null}
{!review.usable && review.kind !== "ready" ? <p role="status">{review.kind === "error" ? copy.refreshError : copy.updating}</p> : null}
{showSourceNotice ? <SourceNotice review={review} copy={copy} mapOnly /> : null}
<GoalWorkMapView map={map} copy={copy.workMap} selectedId={selected}
onSelect={id => {
setLocalSelection(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,23 @@ Task-graph topology now shares `work_items/planning_relations.ts` with inventory
and horizon. One pure TS request owns relationship discovery, deterministic
bounded traversal, edge deduplication and missing/truncated completeness; the
Python predecessor indexes, condition parser and traversal are retired. Python
retains status source adaptation and public-safe node/evidence/handoff rendering.
retains status source adaptation and public-safe node/evidence/handoff rendering
for the selected-work graph. The whole-Goal task map now composes its node kinds,
metadata visibility, edges and public map shape inside the same typed topology
request. Python keeps the shared renderable-source, text/state and stable-ID
codecs; metadata is sanitized only for admitted nodes. Those codecs retire when
the remaining selected-work graph and shared source consumers migrate together.
One map still uses one request. Expanded complete inputs use the existing private
snapshot transport instead of truncating the source at the ordinary wire limit.
The HTTP delivery-review composes its existing reads in one managed-runtime code
revision scope; provider reads and freshness checks retain their owners.
The native map now seals actual Todo-read time, unrenderable/display omissions
and unresolved relations through the existing projection envelope owner.
The HTTP wrapper inherits status and map sources in one coarse sealing request;
this adds one runtime crossing to the complete HTTP read, not per-node calls.
The shipped reader discloses stale, unreadable, partial or unknown sources and
preserves their envelope in exports. Original source opening and authorization
owners remain unchanged; metadata is not a new execution or configuration gate.
This intentionally distinguishes successor lineage from completion dependencies,
corrects unblocks direction, includes Monitor generation conditions and preserves
parallel/diamond edges at the node cap. See the [graph contract](../../reference/protocols/task-graph-projection-v0.md#typed-todo-topology).
Expand Down
Loading
Loading