Task — engineering-spec@1

"DOC lifecycle metadata — parties + effective_date + expiry_date + renewal_terms + parent_contract_id for contract document substrate"

draftTASK-DOC-007
module doc · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-DOC-001 · blocks TASK-DOC-008, TASK-DOC-009

§1 — Description (BCP-14 normative)

The DOC service MUST extend TASK-DOC-001 documents with lifecycle metadata at services/doc/src/lifecycle/ — parties, dates, renewal terms, parent chain, auto-status, 3 memory audit kinds.

  1. MUST define table extension at migration 0002: ``sql ALTER TABLE doc_documents ADD COLUMN parties JSONB; ALTER TABLE doc_documents ADD COLUMN effective_date DATE; ALTER TABLE doc_documents ADD COLUMN expiry_date DATE; ALTER TABLE doc_documents ADD COLUMN renewal_terms JSONB; ALTER TABLE doc_documents ADD COLUMN parent_contract_id UUID REFERENCES doc_documents(document_id); ALTER TABLE doc_documents ADD COLUMN lifecycle_status TEXT CHECK (lifecycle_status IS NULL OR lifecycle_status IN ('draft','active','expiring','expired','terminated','renewed')); ALTER TABLE doc_documents ADD COLUMN status_computed_at TIMESTAMPTZ; CREATE INDEX docs_expiry_idx ON doc_documents(tenant_id, expiry_date) WHERE expiry_date IS NOT NULL; CREATE INDEX docs_parent_idx ON doc_documents(tenant_id, parent_contract_id) WHERE parent_contract_id IS NOT NULL; GRANT UPDATE (parties, effective_date, expiry_date, renewal_terms, parent_contract_id, lifecycle_status, status_computed_at) ON doc_documents TO cyberos_app; ``
  1. MUST validate lifecycle_status against closed enum per DEC-1711.
  1. MUST validate parties JSONB structure per DEC-1713 — array of {party_id, party_type, role}. party_type ∈ {tenant, customer, vendor, employee, authority}.
  1. MUST compute status at status_computer.rs::compute(doc, now) per DEC-1712:
  1. MUST support parent chain per DEC-1714 — amendments link via parent_contract_id; expose tree query endpoint.
  1. MUST expose endpoints: ``text PUT /v1/doc/documents/{id}/lifecycle body: {parties, effective_date, expiry_date, renewal_terms, parent_contract_id?} GET /v1/doc/documents/{id}/lifecycle GET /v1/doc/documents/{id}/parent-chain (returns ancestor tree) ``
  1. MUST emit 3 memory audit kinds per DEC-1715. PII per TASK-MEMORY-111: parties JSON hashed; dates and status enum ok.
  1. MUST thread trace_id from set → compute → audit.
  1. MUST NOT mutate prior parent_contract_id (parent re-link rare; if needed, new row).
  1. MUST NOT skip auto-status on field change per DEC-1712.

§2 — Why this design

Why first-class columns (DEC-1710)? JSONB-only would force every query to parse; columnar = indexable + queryable for TASK-DOC-008.

Why auto-status (DEC-1712)? Manual updates drift; computed status reflects truth at query time.

Why party type enum (DEC-1713)? Roles drive notification routing (employees ≠ customers ≠ authorities).

Why parent chain (DEC-1714)? Amendments form contract lineage; legal needs to see full tree.


§3 — API contract

Sample lifecycle:

{
  "document_id": "uuid",
  "parties": [
    {"party_id": "uuid-tenant", "party_type": "tenant", "role": "service_provider"},
    {"party_id": "uuid-acme", "party_type": "customer", "role": "client"}
  ],
  "effective_date": "2026-01-01",
  "expiry_date": "2027-12-31",
  "renewal_terms": {"auto_renew": true, "notice_days": 60, "term_months": 24},
  "parent_contract_id": null,
  "lifecycle_status": "active",
  "status_computed_at": "2026-05-17T02:00:00Z"
}

Parent chain:

[
  {"document_id": "uuid-amendment-2", "level": 0},
  {"document_id": "uuid-amendment-1", "level": 1},
  {"document_id": "uuid-original-msa", "level": 2}
]

§4 — Acceptance criteria

  1. All lifecycle fields settable. 2. 6-status enum + cardinality test. 3. Auto-status compute on field change. 4. Nightly cron refresh. 5. Parties JSONB structure validated. 6. Party type enum (5: tenant/customer/vendor/employee/authority). 7. Parent chain query returns ancestors. 8. Indexed on expiry_date for TASK-DOC-008. 9. Indexed on parent_contract_id for tree query. 10. 3 memory audit kinds emitted. 11. PII scrubbed (parties JSONB SHA256). 12. RLS denies cross-tenant. 13. Trace_id preserved. 14. Status thresholds correct (90d = expiring). 15. Terminated status manual-only (CLO action). 16. Renewed status set on renewal contract creation. 17. Append-only via REVOKE UPDATE except 7 cols. 18. NULL allowed for legacy docs. 19. Self-referential FK enforced. 20. Status compute idempotent (same input → same result).

§5 — Verification

#[tokio::test]
async fn status_active_within_window() {
    let ctx = TestContext::doc_with_dates("2026-01-01", "2027-12-31").await;
    ctx.compute_status(ctx.doc_id, "2026-06-01").await;
    let d = ctx.fetch_doc(ctx.doc_id).await;
    assert_eq!(d.lifecycle_status.as_deref(), Some("active"));
}

#[tokio::test]
async fn status_expiring_within_90d() {
    let ctx = TestContext::doc_with_dates("2026-01-01", "2026-06-30").await;
    ctx.compute_status(ctx.doc_id, "2026-04-15").await;
    let d = ctx.fetch_doc(ctx.doc_id).await;
    assert_eq!(d.lifecycle_status.as_deref(), Some("expiring"));
}

#[tokio::test]
async fn parent_chain_returns_ancestors() {
    let ctx = TestContext::with_amendment_chain(3).await;
    let chain = ctx.fetch_parent_chain(ctx.leaf_id).await;
    assert_eq!(chain.len(), 3);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-DOC-001. Downstream: TASK-DOC-008 (expiry alerts), TASK-DOC-009 (renewal proposals). Cross-module: TASK-MCP-007 (cron), TASK-AI-003 (parties extraction skill — future), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Invalid party_typevalidatereject 400use valid
Expiry before effectivevalidatereject 400fix dates
Self-referential parentvalidatereject (cycle)fix link
Parent chain >20 deepsanity warnsev-3; allowinherent
Status compute fails on bad datecatchNULL + sev-2fix data
JSONB schema mismatchparsereject 400fix structure
Renewal_terms unparseablevalidatereject 400fix
Cross-tenant parent FKRLS404 (treat as missing)inherent
Concurrent status computelast-writer-winsinherentinherent
Cron skippedcatch on nextinherentinherent

§11 — Implementation notes


End of TASK-DOC-007 spec.