Skip to main content

Implementation Spec — Engagements (Golden Thread Container)

Doc type: Implementation plan (Foundation → Stretch goals)
Status: Draft
Scope: Engagements / Projects as a first-class “Golden Thread” container that groups Assignments, Sessions, Submissions (K/O/E), Evidence events, review/sign-off, and (future) hashing/ratification.
Non-goals: Project management, document repository, HR/performance management.

Tier-1 alignment: This spec MUST not violate docs/architecture/FOUNDATION.md invariants:

  • WORM/append-only applies to the Execution Ledger (submissions + ledger events), not all scaffolding.
  • TxManager + RLS are the enforcement boundary.
  • “Edits” to execution records are Amend/Void patterns, not UPDATE/DELETE.

0. Definitions (simple, stable)

  • Programme: reusable template/curriculum/path that issues tasks (Assignments) to users.
  • Engagement: instance of real-world work (a client/job/thread) that groups tasks and durable outcomes into one timeline (“Golden Thread”).

Rule: Engagements are not a replacement for Programmes. Engagements MAY be created from a Programme, but are distinct from Programme definition objects.


1. Why Engagements exist (first-class justification)

Even though K/O/E outcomes are already WORM in submissions, you still need a first-class container that:

  1. Creates a stable job identity (client/site/project thread) independent of cohorts/run labels.
  2. Groups many tasks (issued from Programmes + ad-hoc) under one umbrella.
  3. Supports glass-box sharing by scoping “what the client can see” to a single Engagement.
  4. Provides a natural boundary for milestone proofs (future hashing + ratification) and “state at time” attestation.
  5. Avoids “run_label as a product feature” (run_label is a reporting tag; Engagement is a domain object with lifecycle, permissions, and events).

2. Data classification (WORM vs scaffolding)

2.1 Execution Ledger (WORM/append-only)

These remain the durable source of truth:

  • submissions (+ submission_items, score versions, approvals, evidence metadata events, etc.)
  • Ledger “event” tables (append-only) that represent amendments/voids/verifications/ratifications.

2.2 Operational scaffolding (mutable by design)

Engagement “current state” can be mutable:

  • engagements row may be updated for status/title/metadata
  • BUT all meaningful changes MUST also be recorded as append-only engagement events.

Pattern (required): Mutable “current state” + append-only “event history”.


3. Foundation (MVP) — Minimal tables, maximum leverage

3.1 Schema (required)

A) engagements (scaffolding table; mutable)

Columns (minimum)

  • id UUID PK
  • tenant_id UUID NOT NULL
  • org_unit_id UUID NOT NULL
  • title TEXT NOT NULL
  • status TEXT NOT NULL (draft|active|paused|closed|archived)
  • created_by_user_id UUID NOT NULL
  • created_at TIMESTAMPTZ NOT NULL
  • updated_at TIMESTAMPTZ NOT NULL

Optional (but recommended now)

  • client_ref TEXT NULL (customer’s internal job ref, not “client identity” in the platform)
  • description TEXT NULL
  • source_program_id UUID NULL (FK to programs.id)
  • source_program_version_checksum TEXT NULL (only if you don’t version programmes; store the checksum or snapshot marker to explain “what template was used”)
  • metadata JSONB NOT NULL DEFAULT '{}'::jsonb (small, non-PII job metadata)

RLS

  • Must follow standard tenant+org unit policies (same pattern as other siloed tables).

B) engagement_events (append-only)

Purpose: defensible timeline of Engagement lifecycle and actions.

Columns (minimum)

  • id UUID PK
  • tenant_id UUID NOT NULL
  • org_unit_id UUID NOT NULL
  • engagement_id UUID NOT NULL REFERENCES engagements(id)
  • event_type TEXT NOT NULL
  • payload JSONB NOT NULL DEFAULT '{}'::jsonb
  • actor_user_id UUID NOT NULL
  • created_at TIMESTAMPTZ NOT NULL

Rules

  • INSERT-only.
  • No UPDATE/DELETE.
  • Used for audit timelines and “what changed, who changed it, when, why”.

Event types (minimum set)

  • engagement.created
  • engagement.renamed
  • engagement.status_changed
  • engagement.metadata_updated (careful: keep payload small)
  • engagement.program_applied (when created-from / applied template)

C) Linkage fields (required)

Add nullable FKs:

  • assignments.engagement_id UUID NULL REFERENCES engagements(id)
  • submissions.engagement_id UUID NULL REFERENCES engagements(id)

Why on submissions too?

  • Submissions are durable and used by reporting.
  • Assignments are scaffolding; they can be archived or reissued.
  • Stamping engagement_id onto submissions makes engagement-level reporting and portal scopes stable.

Session linkage (optional for MVP)

  • delivery_sessions.engagement_id MAY be omitted if derivable from assignment.
  • If included, treat it as scaffolding (mutable/retention-bound).

3.2 Services + queries (required)

Create a dedicated domain service:

  • EngagementsService (new module boundary)

sqlc queries

  • Create engagement
  • Get engagement by id (RLS-scoped)
  • List engagements (filters: status, created_at range)
  • Update engagement scaffolding fields (title/status/metadata)
  • Insert engagement_event
  • Read engagement timeline summary (joins into submissions/assignments)

3.3 API surface (required)

Engagement CRUD

  • POST /api/v1/engagements
  • GET /api/v1/engagements
  • GET /api/v1/engagements/{engagementId}
  • PATCH /api/v1/engagements/{engagementId} (limited fields)
  • GET /api/v1/engagements/{engagementId}/timeline

Timeline output (MVP)

  • Engagement events (from engagement_events)
  • Submission list (from submissions filtered by engagement_id), with:
    • submission_id
    • assignment_id
    • evaluation_version_id
    • kind (K/O/E)
    • completed_at
    • approval/review status if present
    • minimal “headline” fields for UI

3.4 Capability model (required)

Add first-class capabilities (names are canonical-ish; align final names to your capability taxonomy):

  • engagements.read
  • engagements.create
  • engagements.update (rename/status/metadata)
  • engagements.close (explicit close action if you want tighter policy)
  • engagements.timeline.view
  • engagements.assignments.manage (adding tasks to engagement)

Rule: No route may check roles directly; only capabilities.

3.5 Lifecycle rules (required)

  • Engagements have a status. Assignments can be issued only when status allows (policy below).
  • All status changes MUST create an engagement.status_changed event.
  • Renames MUST create an engagement.renamed event.

Policy (MVP)

  • draft: no assignments unless you explicitly allow
  • active: assignments allowed
  • paused: no new assignments; execution of existing assignments MAY be allowed (decide later)
  • closed: no new assignments; portal read-only (future)
  • archived: hidden by default

3.6 Programme relationship (MVP-compatible)

Goal: Minimal churn with your existing programme worker/outbox.

Two supported patterns:

Add endpoint:

  • POST /api/v1/engagements/\{id\}/apply-programme

Behavior:

  1. Validate capability engagements.assignments.manage.
  2. Record engagement.program_applied event with program_id + requirement count.
  3. Use existing orchestration to issue assignments, but stamp engagement_id on each created assignment.

Implementation detail:

  • Extend AssignmentsService.CreateAssignment(...) to accept an optional engagement_id.
  • Extend Programme orchestrator/worker path to pass engagement_id through when present.

Pattern B: “Create Engagement from Programme”

  • POST /api/v1/engagements/from-programme Creates engagement + applies programme in one shot.

Rule: Regardless of pattern, programme requirements remain defined by Programme tables. Engagement is the instance container.

3.7 Copy engagement_id into submissions (required)

On submit (ResultsService.SubmitSession / submission creation):

  • Derive engagement_id from the assignment (or session) and set it on the submission.
  • This preserves “golden thread” even if assignment lifecycle changes later.

3.8 Tests (required)

Add integration tests that prove:

  1. Engagement is RLS-scoped by tenant+org.
  2. Engagement events are append-only (no update paths).
  3. Assignments created from programme contain engagement_id.
  4. Submission created from assignment contains engagement_id.
  5. Timeline endpoint returns both events and submissions.

4. First-class UX/Concept rules (so it doesn’t become a “tag”)

Even without a frontend, the backend must treat Engagements as first-class by ensuring:

  • Dedicated table + service + endpoints
  • Explicit capabilities
  • Explicit lifecycle + events
  • Reporting and queries can filter by engagement_id
  • Programme issuance can target engagement instances
  • Submissions permanently record engagement_id

5. Phase 2 — Operational richness (still not “project management”)

If you need to restrict visibility without introducing “client scope” in RLS:

  • Add engagement_participants table that maps users to engagement roles.

Example:

  • engagement_participants(engagement_id, user_id, role_type, created_at) Role types: operator, verifier, viewer, manager

Important: This is authorization-layer logic, not RLS changes beyond tenant+org. RLS still enforces the hard boundary; capability checks + join checks enforce the engagement-specific access.

5.2 Engagement-level assignment templates (optional)

Allow ad-hoc tasks under an engagement while still using your K/O/E evaluation versions:

  • POST /engagements/\{id\}/assignments (already in MVP surface as optional)
  • Supports issuing:
    • to a user
    • to an email invite flow (if you support it)
    • with run_label override (still useful for analytics)

5.3 Timeline UX contract

The timeline should be able to show:

  • Engagement events
  • K/O/E submissions
  • Evidence/approval events (when implemented)
  • Amendments/voids (ledger truth)
  • “Current state” derived from latest events

5.4 Reporting read models

Add engagement dimension to reporting tables (if needed for performance):

  • report_submission_summary.engagement_id
  • Ensure projection worker populates it from submissions.

6. Phase 3 — Glass Box access (client visibility, still tenant+org boundary)

Even though you’re simplifying wording to tenant+org only, you can still offer a client/stakeholder portal scoped at engagement level using:

  • magic-link viewer tokens with scope: { type: engagement, id: \<engagementId\> }
  • capabilities: engagements.timeline.view, ledger.view, optional ledger.ratify

Rules

  • Read-only by default.
  • “Named / sensitive” fields are controlled by disclosure rules (Tier 1 + policy).
  • All reads still go through TxManager + RLS.

7. Stretch goals — Hashing and Ratification (milestone proofs)

7.1 Hashing (state fingerprint)

Add an append-only table:

  • engagement_state_hashes
    • engagement_id
    • milestone_key (e.g., “handover-1”)
    • state_hash (sha256 of canonical, deterministic representation)
    • computed_at, computed_by_user_id
    • inputs JSONB (what was included: submission ids, versions, etc.)

Determinism rule

  • Same ledger state → same hash.
  • Define canonical ordering (by submission completed_at, then id, etc.).

7.2 Ratification (stakeholder sign-off)

Add append-only table:

  • engagement_ratifications
    • references engagement_state_hashes.id
    • signer identity
    • step-up method used
    • signed_at
    • optional comment

Contract

  • Ratification requires step-up auth (policy-driven).
  • Ratification never edits state; it appends an event.

7.3 Where it fits in your system

  • Engagement becomes the natural boundary for “milestone proof”.
  • Submissions remain the atomic WORM records.
  • Hashing/ratification prove: “this exact set of records existed and was acknowledged at this time”.

8. Implementation sequencing (why this order)

  1. Schema + RLS + sqlc (foundation for first-class domain)
  2. Service + API endpoints (create/list/update + timeline)
  3. Stamp engagement_id into assignments + submissions (makes it real)
  4. Programme apply path (integrates with existing orchestration without churn)
  5. Tests + projection updates (keeps reporting coherent)
  6. Phase 2 participants + richer timeline (operational completeness)
  7. Glass Box + hashing/ratification (premium defensibility features)

9. “Done” definition (MVP)

Engagements are considered first-class when:

  • A new Engagement can be created and updated (status/title) with an event history
  • Programme issuance can target an Engagement and produce assignments with engagement_id
  • Submissions created from those assignments store engagement_id
  • Timeline endpoint returns a coherent engagement story (events + submissions)
  • Capability checks exist and RLS remains tenant+org enforced
  • Integration tests cover the whole flow end-to-end

10. Open questions (safe defaults)

  1. Do we require engagement_id on all assignments?
    • Default: NO (nullable), because not all work must be in an engagement.
  2. Do we require engagement_id on all submissions?
    • Default: NO, but copy it when available. This avoids blocking existing flows.
  3. Should engagement status block session launch?
    • Default: If status is closed|archived, block new session creation for its assignments.
  4. Programme vs Engagement naming
    • Internally keep Programmes as template/curriculum.
    • Externally you may present Engagement as “Project/Job” depending on ICP, without changing DB names.