Skip to content

Commit 425c90e

Browse files
committed
[feat.dirac.Ω.D9+] route one Program job through every stage
A scientific job must keep one identity while its context moves from Understand through Design, Decide, Make, and Test & Learn. This commit separates that stable Work Item from immutable specification revisions and runtime Job attempts, enforces one-Work-Item ownership per runtime Job, and projects the active lane into every stage including the Mol* scene shell. Copy-per-Workspace task records were rejected because they destroy ownership, provenance, and handoff continuity. Concurrent Motif compute work remains unstaged.
1 parent ca04c71 commit 425c90e

21 files changed

Lines changed: 722 additions & 60 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
-- 028 · One Program Work Item moves through the discovery workflow without copies.
2+
--
3+
-- A Work Item is the stable scientific unit of intent. Work Packages are immutable
4+
-- revisions of its specification; runtime Jobs are execution attempts attached to it.
5+
-- This prevents Program, Design, Decide, Make and Test from minting private task IDs.
6+
7+
ALTER TYPE app.object_kind ADD VALUE IF NOT EXISTS 'work_item';
8+
9+
BEGIN;
10+
11+
CREATE TYPE design.workflow_lane AS ENUM (
12+
'understand', 'design', 'decide', 'make', 'test_learn'
13+
);
14+
15+
CREATE TABLE design.program_work_item (
16+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
17+
program_id uuid NOT NULL REFERENCES design.project(id) ON DELETE RESTRICT,
18+
work_key citext NOT NULL,
19+
title text NOT NULL CHECK (btrim(title) <> ''),
20+
current_lane design.workflow_lane NOT NULL DEFAULT 'understand',
21+
current_package_id uuid,
22+
created_at timestamptz NOT NULL DEFAULT now(),
23+
created_by_kind app.actor_kind NOT NULL,
24+
created_by_id text NOT NULL,
25+
UNIQUE (program_id, work_key)
26+
);
27+
28+
-- Existing Work Packages become revisions of one newly-minted stable Work Item.
29+
INSERT INTO design.program_work_item(program_id,work_key,title,created_at,created_by_kind,created_by_id)
30+
SELECT DISTINCT ON (program_id,work_key)
31+
program_id,work_key,title,created_at,created_by_kind,created_by_id
32+
FROM design.program_work_package
33+
ORDER BY program_id,work_key,revision;
34+
35+
ALTER TABLE design.program_work_package ADD COLUMN work_item_id uuid;
36+
UPDATE design.program_work_package package
37+
SET work_item_id = item.id
38+
FROM design.program_work_item item
39+
WHERE item.program_id=package.program_id AND item.work_key=package.work_key;
40+
ALTER TABLE design.program_work_package
41+
ALTER COLUMN work_item_id SET NOT NULL,
42+
ADD CONSTRAINT program_work_package_item_fk
43+
FOREIGN KEY (work_item_id) REFERENCES design.program_work_item(id) ON DELETE RESTRICT,
44+
ADD CONSTRAINT program_work_package_item_revision_unique UNIQUE (work_item_id,revision);
45+
46+
UPDATE design.program_work_item item
47+
SET current_package_id = current.id,
48+
title = current.title
49+
FROM (
50+
SELECT DISTINCT ON (work_item_id) id,work_item_id,title
51+
FROM design.program_work_package
52+
ORDER BY work_item_id,revision DESC
53+
) current
54+
WHERE current.work_item_id=item.id;
55+
ALTER TABLE design.program_work_item
56+
ADD CONSTRAINT program_work_item_current_package_fk
57+
FOREIGN KEY (current_package_id) REFERENCES design.program_work_package(id) ON DELETE RESTRICT;
58+
59+
CREATE TABLE design.program_work_transition (
60+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
61+
work_item_id uuid NOT NULL REFERENCES design.program_work_item(id) ON DELETE RESTRICT,
62+
from_lane design.workflow_lane,
63+
to_lane design.workflow_lane NOT NULL,
64+
reason text NOT NULL CHECK (btrim(reason) <> ''),
65+
transitioned_at timestamptz NOT NULL DEFAULT now(),
66+
transitioned_by_kind app.actor_kind NOT NULL,
67+
transitioned_by_id text NOT NULL,
68+
CHECK (from_lane IS NULL OR from_lane <> to_lane)
69+
);
70+
CREATE INDEX program_work_transition_item
71+
ON design.program_work_transition(work_item_id,transitioned_at DESC);
72+
73+
INSERT INTO design.program_work_transition(
74+
work_item_id,from_lane,to_lane,reason,transitioned_at,transitioned_by_kind,transitioned_by_id)
75+
SELECT id,NULL,current_lane,'Migrated into the canonical Program workflow',created_at,created_by_kind,created_by_id
76+
FROM design.program_work_item;
77+
78+
CREATE TABLE design.program_work_item_dependency (
79+
work_item_id uuid NOT NULL REFERENCES design.program_work_item(id) ON DELETE RESTRICT,
80+
depends_on_work_item_id uuid NOT NULL REFERENCES design.program_work_item(id) ON DELETE RESTRICT,
81+
PRIMARY KEY (work_item_id,depends_on_work_item_id),
82+
CHECK (work_item_id <> depends_on_work_item_id)
83+
);
84+
85+
INSERT INTO design.program_work_item_dependency(work_item_id,depends_on_work_item_id)
86+
SELECT DISTINCT package.work_item_id,dependency.work_item_id
87+
FROM design.program_work_dependency edge
88+
JOIN design.program_work_package package ON package.id=edge.work_package_id
89+
JOIN design.program_work_package dependency ON dependency.id=edge.depends_on_id
90+
ON CONFLICT DO NOTHING;
91+
92+
CREATE TABLE design.program_work_execution (
93+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
94+
work_item_id uuid NOT NULL REFERENCES design.program_work_item(id) ON DELETE RESTRICT,
95+
job_id uuid NOT NULL UNIQUE REFERENCES app.job(id) ON DELETE RESTRICT,
96+
purpose text,
97+
linked_at timestamptz NOT NULL DEFAULT now(),
98+
linked_by_kind app.actor_kind NOT NULL,
99+
linked_by_id text NOT NULL
100+
);
101+
CREATE INDEX program_work_execution_item
102+
ON design.program_work_execution(work_item_id,linked_at DESC);
103+
104+
CREATE FUNCTION app.sync_program_work_item_trigger() RETURNS trigger LANGUAGE plpgsql AS $$
105+
BEGIN
106+
PERFORM app.register_entity(
107+
'work_item',NEW.id::text,NEW.program_id::text||':'||NEW.work_key::text,
108+
NEW.title,'design','program_work_item');
109+
RETURN NEW;
110+
END $$;
111+
112+
SELECT app.register_entity(
113+
'work_item',id::text,program_id::text||':'||work_key::text,title,
114+
'design','program_work_item')
115+
FROM design.program_work_item;
116+
CREATE TRIGGER entity_program_work_item AFTER INSERT OR UPDATE ON design.program_work_item
117+
FOR EACH ROW EXECUTE FUNCTION app.sync_program_work_item_trigger();
118+
119+
ALTER TABLE design.program_evidence_binding
120+
DROP CONSTRAINT program_evidence_binding_subject_kind_check,
121+
ADD CONSTRAINT program_evidence_binding_subject_kind_check CHECK (
122+
subject_kind IN ('program','objective','hypothesis','decision','milestone',
123+
'stage_gate','work_item','work_package'));
124+
125+
ALTER TABLE design.program_event DROP CONSTRAINT program_event_event_kind_check;
126+
ALTER TABLE design.program_event ADD CONSTRAINT program_event_event_kind_check CHECK (event_kind IN (
127+
'program.created','program.updated','objective.recorded','hypothesis.recorded',
128+
'decision.recorded','milestone.recorded','object.linked','snapshot.created',
129+
'portfolio.assigned','member.assigned','stage_gate.recorded','work_package.recorded',
130+
'work_item.transitioned','work_execution.linked','evidence.attached','lineage.recorded'
131+
));
132+
133+
COMMENT ON TABLE design.program_work_item IS
134+
'Stable Program job identity. It moves between workflow lanes; it is never copied per Workspace.';
135+
COMMENT ON TABLE design.program_work_package IS
136+
'Immutable revision of a stable Program Work Item specification.';
137+
COMMENT ON TABLE design.program_work_execution IS
138+
'A runtime Job may belong to exactly one Program Work Item; a Work Item may have many attempts.';
139+
140+
INSERT INTO meta.migration (filename, sha256, content_sha256, hash_source)
141+
VALUES ('028_unique_program_work_items.sql', '\x1f10e56fc3d17f816ad4578314639817dda18cc686607159c42976589f6b0e44'::bytea,
142+
'\x1f10e56fc3d17f816ad4578314639817dda18cc686607159c42976589f6b0e44'::bytea, 'content')
143+
ON CONFLICT (filename) DO NOTHING;
144+
145+
COMMIT;

‎backend/dirac_app/handlers.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,18 @@ def program_work_package_record(input: dict, ctx) -> dict:
285285
ctx.actor, ctx.request_id)
286286

287287

288+
def program_work_item_transition(input: dict, ctx) -> dict:
289+
return _programs(ctx).transition_work_item(
290+
input["program_ref"], input["expected_version"], input["transition"],
291+
ctx.actor, ctx.request_id)
292+
293+
294+
def program_work_execution_attach(input: dict, ctx) -> dict:
295+
return _programs(ctx).attach_work_execution(
296+
input["program_ref"], input["expected_version"], input["execution"],
297+
ctx.actor, ctx.request_id)
298+
299+
288300
def program_evidence_attach(input: dict, ctx) -> dict:
289301
return _programs(ctx).attach_evidence(
290302
input["program_ref"], input["expected_version"], input["binding"],

‎backend/programs/domain.py‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
})
3232
GATE_STATUSES = frozenset({"planned", "ready", "approved", "rejected"})
3333
WORK_STATUSES = frozenset({"backlog", "ready", "active", "blocked", "done", "cancelled"})
34+
WORKFLOW_LANES = frozenset({"understand", "design", "decide", "make", "test_learn"})
3435
EVIDENCE_RELATIONS = frozenset({"supports", "contradicts", "tests", "explains"})
3536
EVIDENCE_KINDS = frozenset({
3637
"evidence", "measurement", "dataset", "artifact", "literature_reference",
@@ -223,6 +224,9 @@ def work_package(value: dict[str, Any]) -> dict[str, Any]:
223224
status = value.get("status", "backlog")
224225
if status not in WORK_STATUSES:
225226
raise failures.DiracInvalidParameters("unknown work package status")
227+
lane = value.get("lane", "understand")
228+
if lane not in WORKFLOW_LANES:
229+
raise failures.DiracInvalidParameters("unknown Program workflow lane")
226230
priority = value.get("priority", 3)
227231
if not isinstance(priority, int) or isinstance(priority, bool) or not 1 <= priority <= 5:
228232
raise failures.DiracInvalidParameters("work_package.priority must be an integer from 1 to 5")
@@ -234,19 +238,38 @@ def work_package(value: dict[str, Any]) -> dict[str, Any]:
234238
return {"key": key(value.get("key"), "work_package.key"),
235239
"title": nonempty(value.get("title"), "work_package.title", maximum=256),
236240
"description": nonempty(value.get("description"), "work_package.description"),
237-
"status": status, "priority": priority,
241+
"lane": lane, "status": status, "priority": priority,
238242
"owner": actor(owner) if owner is not None else None,
239243
"due_on": _iso_date(value.get("due_on"), "work_package.due_on"),
240244
"deliverable_refs": [ref(item) for item in deliverables],
241-
"depends_on_refs": [ref(item, "work_package") for item in dependencies]}
245+
"depends_on_refs": [ref(item, "work_item") for item in dependencies]}
246+
247+
248+
def work_transition(value: dict[str, Any]) -> dict[str, Any]:
249+
if not isinstance(value, dict):
250+
raise failures.DiracInvalidParameters("transition must be an object")
251+
lane = value.get("to_lane")
252+
if lane not in WORKFLOW_LANES:
253+
raise failures.DiracInvalidParameters("unknown Program workflow lane")
254+
return {"work_item_ref": ref(value.get("work_item_ref"), "work_item"),
255+
"to_lane": lane,
256+
"reason": nonempty(value.get("reason"), "transition.reason", maximum=4000)}
257+
258+
259+
def work_execution(value: dict[str, Any]) -> dict[str, Any]:
260+
if not isinstance(value, dict):
261+
raise failures.DiracInvalidParameters("execution binding must be an object")
262+
return {"work_item_ref": ref(value.get("work_item_ref"), "work_item"),
263+
"job_ref": ref(value.get("job_ref"), "job"),
264+
"purpose": _optional_text(value.get("purpose"), 2000)}
242265

243266

244267
def evidence_binding(value: dict[str, Any]) -> dict[str, Any]:
245268
if not isinstance(value, dict):
246269
raise failures.DiracInvalidParameters("binding must be an object")
247270
subject = ref(value.get("subject_ref"))
248271
evidence = ref(value.get("evidence_ref"))
249-
if subject["kind"] not in {"program", "objective", "hypothesis", "decision", "milestone", "stage_gate", "work_package"}:
272+
if subject["kind"] not in {"program", "objective", "hypothesis", "decision", "milestone", "stage_gate", "work_item", "work_package"}:
250273
raise failures.DiracInvalidParameters("unsupported evidence subject kind")
251274
if evidence["kind"] not in EVIDENCE_KINDS:
252275
raise failures.DiracInvalidParameters("unsupported evidence object kind")

0 commit comments

Comments
 (0)