Version. 0.1
Date. 2026-05-04
Author. Marvin Percival (DUNIN7), prepared via Claude.
Target. /Users/dunin7/loomworks (substrate) on DUNIN7-M4 (MacMini).
Priority. Standard.
Confidential. Internal.
Supersedes. Nothing for Phase 39 specifically.
Companion to. Phase 39 scoping note v0.1, engine implementation strategy v0.2 §9, Phase 14 CR v0.1 (person layer and memberships), Phase 33 CR v0.1 (per-engagement activity summary).
Status. First draft. Consumes settled scoping decisions from scoping note v0.1 without relitigating them.
Phase 39 adds three endpoints under /me/dashboard/ that aggregate state across all the requesting person's engagements. Today every endpoint is single-engagement-scoped. The Operator Layer Dashboard needs a single-call cross-engagement view of active jobs, items needing decision, and recently finished artifacts.
Three endpoints:
1 — GET /me/dashboard/active — Active jobs across all engagements. Shape production, render production, external polling.
2 — GET /me/dashboard/needs_you — Items requiring the person's decision. Pending shapes (awaiting confirmation), held assertions (awaiting commit), produced renders (awaiting review).
3 — GET /me/dashboard/recent — Recently finished artifacts. Completed renders with download URLs where applicable.
No migrations. No new MemoryObject types. Pure endpoint addition querying existing tables through the person's memberships.
Substrate-only phase. One checkpoint.
Phase 39 is grounded in three layers, consulted in order.
Layer 1 — Methodology document v0.20. Recognition is load-bearing: the cross-engagement view makes the Operator's full portfolio visible in one surface, not fragmented across N engagement dashboards. Plain-English communication: the response schemas carry human-readable labels and context alongside IDs.
Layer 2 — Reference Design v0.1.2. The person layer (Phase 14) establishes that a person has memberships across engagements. Phase 39 aggregates state across those memberships.
Layer 3 — Specification (Playground Spec v0.10 and prior phase CRs). Phase 14 (person layer): GET /me/memberships returns the person's engagement list. Phase 33 (activity observability): GET /engagements/{eid}/activity-summary returns per-engagement counts. Phase 39 replaces N concurrent per-engagement calls with one aggregated call per dashboard zone.
GET /me/memberships operational. EngagementMembership model with designation field.GET /engagements/{eid}/activity-summary operational.CC confirms before Step 1:
uv run pytest -v reports the expected passed count. If the count differs significantly, stop and reconcile.GET /me/memberships endpoint location identified. Confirm response includes engagement_id, engagement_name (or title), and designation.EngagementMembership model location identified. Confirm designation field structure (enum, string, or list).shaping_jobs operational table location identified. Confirm status column with queued and dispatched values. Confirm engagement_id column.render_jobs operational table location identified. Confirm status column and engagement_id column.external_production_records table location identified (Phase 34). Confirm status column.state column with pending_confirmation value.state column with held value.state column with produced value./me/dashboard/... endpoints.activity-summary endpoint location identified for reference.
Archive this CR to docs/phase-crs/phase-39-cr-cross-engagement-aggregation-v0_1.md at Step 0 before Step 1 begins.
All item kinds from the strategy doc are included in the Literal types now:
active item kinds: shape_production, render_production, composition, external_polling.needs_you item kinds: pending_shape, held_assertion, produced_render_for_review, review_report, external_failure, downstream_specification.recent artifact kinds: render, review_report, transformed_artifact.Kinds without backing data (composition, review_report, downstream_specification, transformed_artifact) return no items. The schemas are forward-compatible for Phases 37 and 40.
At pre-flight, CC checks whether the designation field on EngagementMembership carries values that distinguish read-only from full membership. If it does, needs_you filters out engagements where the person has read-only access. If all memberships are currently equal, the filtering is noted as a future refinement in implementation notes.
Each endpoint runs a single SQL query with UNIONs joined through the membership table. Each arm selects from one source table filtered by the person's memberships. The UNION result is sorted by recency and paginated.
If the UNION approach proves unwieldy at pre-flight (different column shapes), CC falls back to N queries with in-memory merge-sort and notes the choice in implementation notes.
Each endpoint completes within 200ms for a person with 10 engagements. CC benchmarks at pre-flight. If the benchmark exceeds budget, CC adds indexes on join columns before proceeding.
Each endpoint accepts ?limit (default 50, max 100) and ?cursor (opaque base64-encoded timestamp). The cursor keys on started_at (active), created_at (needs_you), or completed_at (recent).
Returns active jobs across all the requesting person's engagements.
Source tables: shaping_jobs (status IN queued, dispatched), render_jobs (status IN queued, dispatched), external_production_records (status = awaiting_external).
Each item carries: engagement_id, engagement_name, item_kind, item_id (job ID), item_label (human-readable), started_at, progress_hint (nullable).
Sorted by started_at descending (most recent first).
Returns items requiring the person's decision.
Source views: shape events (state = pending_confirmation), assertions (state = held), render events (state = produced, where "produced" implies awaiting Operator review before the artifact is considered final).
Each item carries: engagement_id, engagement_name, item_kind, item_id, item_label, detail (brief context), created_at.
Sorted by created_at descending.
Designation-aware filtering: if the person's membership on an engagement is read-only, items from that engagement are excluded from needs_you (the person cannot act on them).
Returns recently finished artifacts.
Source views: render events (state = produced, with completed_at or created_at as the recency key).
Each item carries: engagement_id, engagement_name, artifact_kind, artifact_id, artifact_label (render title or display number), completed_at, download_url (nullable — populated for renders with downloadable content per Phase 35 content kinds).
Sorted by completed_at descending.
All three endpoints share a common membership subquery:
person_engagements = (
select(EngagementMembership.engagement_id)
.where(EngagementMembership.person_id == current_person.id)
)
For needs_you, this subquery additionally filters by designation if the designation model supports it.
The active endpoint UNIONs across three source tables:
SELECT engagement_id, 'shape_production' as item_kind, id as item_id,
label, started_at, progress_hint
FROM shaping_jobs
WHERE engagement_id IN (person_engagements)
AND status IN ('queued', 'dispatched')
UNION ALL
SELECT engagement_id, 'render_production', id, label, started_at, progress_hint
FROM render_jobs
WHERE engagement_id IN (person_engagements)
AND status IN ('queued', 'dispatched')
UNION ALL
SELECT engagement_id, 'external_polling', id, label, started_at, NULL
FROM external_production_records
WHERE engagement_id IN (person_engagements)
AND status = 'awaiting_external'
ORDER BY started_at DESC
LIMIT :limit
CC adapts the SQL to the actual column names and table structures found at pre-flight. The label column may need to be composed from available fields (shape type name, render type name, etc.).
Each endpoint accepts:
limit — integer, default 50, max 100.cursor — opaque string, base64-encoded timestamp. Omitted on first request.Response includes:
class PaginatedResponse(BaseModel):
items: list[...]
total_count: int
next_cursor: str | None = None # None when no more pages
The cursor is the timestamp of the last item in the current page. The next request filters < cursor_timestamp for descending sort.
from datetime import datetime
from uuid import UUID
from typing import Literal
from pydantic import BaseModel
# --- Active ---
class DashboardActiveItem(BaseModel):
engagement_id: UUID
engagement_name: str
item_kind: Literal[
"shape_production", "render_production",
"composition", "external_polling"
]
item_id: UUID
item_label: str
started_at: datetime
progress_hint: str | None = None
class DashboardActiveResponse(BaseModel):
items: list[DashboardActiveItem]
total_count: int
next_cursor: str | None = None
# --- Needs You ---
class DashboardNeedsYouItem(BaseModel):
engagement_id: UUID
engagement_name: str
item_kind: Literal[
"pending_shape", "held_assertion",
"produced_render_for_review", "review_report",
"external_failure", "downstream_specification"
]
item_id: UUID
item_label: str
detail: str
created_at: datetime
class DashboardNeedsYouResponse(BaseModel):
items: list[DashboardNeedsYouItem]
total_count: int
next_cursor: str | None = None
# --- Recent ---
class DashboardRecentItem(BaseModel):
engagement_id: UUID
engagement_name: str
artifact_kind: Literal[
"render", "review_report", "transformed_artifact"
]
artifact_id: UUID
artifact_label: str
completed_at: datetime
download_url: str | None = None
class DashboardRecentResponse(BaseModel):
items: list[DashboardRecentItem]
total_count: int
next_cursor: str | None = None
test_active_single_engagement — Person with one engagement, one active shaping job. Verify active returns one item with correct engagement context.
test_active_multiple_engagements — Person with two engagements, active jobs in both. Verify items from both engagements returned, sorted by started_at.
test_active_no_membership — Create an engagement the person is not a member of. Create an active job in it. Verify it does not appear.
test_active_empty — Person with engagements but no active jobs. Verify empty list, not error.
test_active_external_polling — Active ExternalProductionRecord in awaiting_external state. Verify it appears as external_polling kind.
test_needs_you_pending_shapes — Engagement with pending shapes. Verify they appear as pending_shape items.
test_needs_you_held_assertions — Engagement with held assertions. Verify they appear as held_assertion items.
test_needs_you_cross_engagement — Person with multiple engagements, pending items in several. Verify aggregation.
test_needs_you_empty — No pending items. Verify empty list.
test_needs_you_read_only_filtered — If designation supports it: person with read-only membership on an engagement. Verify that engagement's items are excluded. (If designation does not distinguish, this test is skipped with a note.)
test_recent_completed_renders — Engagement with completed renders. Verify they appear as render artifacts.
test_recent_with_download_url — Render with downloadable content (binary_blob or multi_file). Verify download_url populated.
test_recent_without_download_url — Render with inline_dict content. Verify download_url is null.
test_recent_cross_engagement — Multiple engagements, recent artifacts in several. Verify aggregation sorted by completed_at.
test_pagination_limit — Create 10 items. Request with limit=3. Verify 3 items returned. Verify next_cursor is not null.
test_pagination_cursor — Use next_cursor from prior request. Verify next page of results.
test_pagination_exhausted — Request last page. Verify next_cursor is null.
test_unauthenticated_401 — Request without session. Verify 401.
test_performance_10_engagements — Create 10 engagements with membership. Populate with items. Verify each endpoint completes within 200ms.
Auto-mode posture: Steps 1–6 auto-mode-proceed. Checkpoint A halts until Operator confirms.
Archive this CR. Run pre-flight checks (Section 3.2). Confirm baseline. Benchmark membership query performance.
Verification: test count matches expectations. Alembic single head. CR archived. All pre-flight items confirmed.
Commit: Phase 39 step 0: CR archival and pre-flight.
Create src/loomworks/api/schemas/dashboard.py with all six response models (three items + three responses). Register in schema exports.
Verification: uv run pytest -v green (no behavioral change — schemas only).
Commit: Phase 39 step 1: dashboard response schemas.
Implement the active endpoint in a new router file src/loomworks/api/routers/dashboard.py. Membership-scoped UNION query. Pagination. Mount on the app.
Write: test_active_single_engagement, test_active_multiple_engagements, test_active_no_membership, test_active_empty, test_active_external_polling.
Verification: uv run pytest -v green.
Commit: Phase 39 step 2: active endpoint.
Implement the needs_you endpoint. Membership-scoped query. Designation-aware filtering if applicable.
Write: test_needs_you_pending_shapes, test_needs_you_held_assertions, test_needs_you_cross_engagement, test_needs_you_empty, test_needs_you_read_only_filtered.
Verification: uv run pytest -v green.
Commit: Phase 39 step 3: needs_you endpoint.
Implement the recent endpoint. Membership-scoped query. Download URL population for applicable content kinds.
Write: test_recent_completed_renders, test_recent_with_download_url, test_recent_without_download_url, test_recent_cross_engagement.
Verification: uv run pytest -v green.
Commit: Phase 39 step 4: recent endpoint.
Add cursor-based pagination to all three endpoints. ?limit and ?cursor query params.
Write: test_pagination_limit, test_pagination_cursor, test_pagination_exhausted.
Verification: uv run pytest -v green.
Commit: Phase 39 step 5: pagination.
Write: test_unauthenticated_401, test_performance_10_engagements.
Verification: uv run pytest -v green. Performance within 200ms budget.
Commit: Phase 39 step 6: auth and performance tests.
Operator confirms. Tag substrate repo.
All Phase 39 work complete. Three dashboard endpoints operational. Pagination working. Performance within budget. All tests green.
Tag: phase-39-cross-engagement-aggregation on DUNIN7/loomworks.
Implementation notes: docs/phase-impl-notes/phase-39-implementation-notes-v0_1.md absorbs execution-time surprises and findings.
GET /me/dashboard/active returns active jobs across person's engagements.GET /me/dashboard/needs_you returns items requiring decision across engagements.GET /me/dashboard/recent returns recently finished artifacts across engagements.?limit and ?cursor.phase-39-cross-engagement-aggregation on substrate repo.src/loomworks/api/routers/dashboard.py.src/loomworks/api/schemas/dashboard.py.
Read the Change Request document at the path I supply below. This is
CR-2026-051 v0.1, the Phase 39 Change Request. You are the executing
agent named in the CR.
CR path: ~/Downloads/phase-39-cr-cross-engagement-aggregation-v0_1.md
Phase 39 adds three cross-engagement aggregation endpoints under
/me/dashboard/: active (jobs), needs_you (items requiring decision),
and recent (finished artifacts). Membership-scoped queries. No
migrations — pure endpoint addition querying existing tables.
Substrate-only phase. Six steps, one checkpoint.
Code baseline: at HEAD after prior phases. Post-prior-phase test count.
Run pre-flight (Step 0) per Section 3.2. The Step 0 checklist
includes: membership model and designation field; operational tables
(shaping_jobs, render_jobs, external_production_records); shape/
assertion/render views with state columns; no existing
/me/dashboard/ endpoints.
Per Section 3.3: archive this CR to
docs/phase-crs/phase-39-cr-cross-engagement-aggregation-v0_1.md
at Step 0 before Step 1 begins.
Per Section 10, six steps with one checkpoint. Auto-mode posture:
Steps 1–6 auto-mode-proceed; Checkpoint A halts for Operator
to confirm and tag.
Performance budget: 200ms per endpoint for 10 engagements.
Benchmark at pre-flight.
Cursor-based pagination with ?limit and ?cursor on all three
endpoints.
Pre-flight surprises (Section 3.2 ground-truth divergence) stop
execution at Step 0 and drive a CR v0.2 revision; do not proceed
through divergence.
Implementation notes at Checkpoint A:
docs/phase-impl-notes/phase-39-implementation-notes-v0_1.md
absorbs execution-time surprises and findings.
DUNIN7 — Done In Seven LLC — Miami, Florida Phase 39: Cross-engagement Aggregation — CR v0.1 — 2026-05-04