"DOC lifecycle metadata — parties + effective_date + expiry_date + renewal_terms + parent_contract_id for contract document substrate"
§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.
- 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;``
- MUST validate
lifecycle_statusagainst closed enum per DEC-1711.
- MUST validate parties JSONB structure per DEC-1713 — array of
{party_id, party_type, role}. party_type ∈ {tenant, customer, vendor, employee, authority}.
- MUST compute status at
status_computer.rs::compute(doc, now)per DEC-1712:
- Triggered on field change + nightly cron (TASK-MCP-007).
- Updates
lifecycle_status+status_computed_at.
- MUST support parent chain per DEC-1714 — amendments link via
parent_contract_id; expose tree query endpoint.
- 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)``
- MUST emit 3 memory audit kinds per DEC-1715. PII per TASK-MEMORY-111: parties JSON hashed; dates and status enum ok.
- MUST thread trace_id from set → compute → audit.
- MUST NOT mutate prior
parent_contract_id(parent re-link rare; if needed, new row).
- 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
- 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
| Failure | Detection | Outcome | Recovery |
|---|---|---|---|
| Invalid party_type | validate | reject 400 | use valid |
| Expiry before effective | validate | reject 400 | fix dates |
| Self-referential parent | validate | reject (cycle) | fix link |
| Parent chain >20 deep | sanity warn | sev-3; allow | inherent |
| Status compute fails on bad date | catch | NULL + sev-2 | fix data |
| JSONB schema mismatch | parse | reject 400 | fix structure |
| Renewal_terms unparseable | validate | reject 400 | fix |
| Cross-tenant parent FK | RLS | 404 (treat as missing) | inherent |
| Concurrent status compute | last-writer-wins | inherent | inherent |
| Cron skipped | catch on next | inherent | inherent |
§11 — Implementation notes
- §11.1 Status computer pure function:
(effective, expiry, now) → status. - §11.2 Nightly cron runs at 03:00 tenant_tz, recomputes all docs with expiry_date set.
- §11.3 Parent chain query recursive CTE:
WITH RECURSIVE chain AS (...). - §11.4 memory audit body: doc_id, status, status_computed_at; parties JSONB hashed.
- §11.5 Future task can ingest contract PDFs and auto-extract parties via TASK-AI-003 — this task provides the schema.
End of TASK-DOC-007 spec.