diff --git a/MANIFEST.yaml b/MANIFEST.yaml index 6f8d6656..9f14fb4c 100644 --- a/MANIFEST.yaml +++ b/MANIFEST.yaml @@ -813,7 +813,7 @@ files: - path: scripts/lib/obligation-first.js role: tooling - hash: sha256:8ea2c5010e3ff91c3cda8e9398a40c4c2ac83baa712337b1b88250c32f11c041 + hash: sha256:868310c404cbd0eab87e045541de95c23a2b9e67194495bc0efa130c1e3319fd note: scripts/lib/obligation-first.js - path: scripts/lib/temporal-status.js @@ -1038,7 +1038,7 @@ files: - path: tests/source-admission.test.js role: code - hash: sha256:d2ec25e0a18c9c83b9ec505fedecf776f2b5971b4f4de24102273681467f45b2 + hash: sha256:0d13f401800e2b95eea010905c6eea6c1c8081ed10d518dc8836931b5c4a6106 note: Source admission policy and producer-gate regressions - path: tests/publication-state.test.js diff --git a/docs/MANIFEST.yaml b/docs/MANIFEST.yaml index 6f8d6656..9f14fb4c 100644 --- a/docs/MANIFEST.yaml +++ b/docs/MANIFEST.yaml @@ -813,7 +813,7 @@ files: - path: scripts/lib/obligation-first.js role: tooling - hash: sha256:8ea2c5010e3ff91c3cda8e9398a40c4c2ac83baa712337b1b88250c32f11c041 + hash: sha256:868310c404cbd0eab87e045541de95c23a2b9e67194495bc0efa130c1e3319fd note: scripts/lib/obligation-first.js - path: scripts/lib/temporal-status.js @@ -1038,7 +1038,7 @@ files: - path: tests/source-admission.test.js role: code - hash: sha256:d2ec25e0a18c9c83b9ec505fedecf776f2b5971b4f4de24102273681467f45b2 + hash: sha256:0d13f401800e2b95eea010905c6eea6c1c8081ed10d518dc8836931b5c4a6106 note: Source admission policy and producer-gate regressions - path: tests/publication-state.test.js diff --git a/docs/api/v1/of/obligations.json b/docs/api/v1/of/obligations.json index af6a200c..f673b349 100644 --- a/docs/api/v1/of/obligations.json +++ b/docs/api/v1/of/obligations.json @@ -20,9 +20,9 @@ "anchors": [ "https://everyailaw.com/obligation-category/transparency.json" ], - "lifecycle_status": "unknown", - "operative_status": "unknown", - "enforcement_status": "unknown", + "lifecycle_status": "draft", + "operative_status": "future", + "enforcement_status": "unsignaled", "jurisdiction": { "@type": "of:Jurisdiction", "territorial_scope": [ diff --git a/docs/api/v1/of/records/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json b/docs/api/v1/of/records/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json index 4d6b3683..2357d56a 100644 --- a/docs/api/v1/of/records/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json +++ b/docs/api/v1/of/records/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json @@ -16,9 +16,9 @@ "anchors": [ "https://everyailaw.com/obligation-category/transparency.json" ], - "lifecycle_status": "unknown", - "operative_status": "unknown", - "enforcement_status": "unknown", + "lifecycle_status": "draft", + "operative_status": "future", + "enforcement_status": "unsignaled", "jurisdiction": { "@type": "of:Jurisdiction", "territorial_scope": [ diff --git a/docs/obligation/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json b/docs/obligation/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json index 4d6b3683..2357d56a 100644 --- a/docs/obligation/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json +++ b/docs/obligation/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json @@ -16,9 +16,9 @@ "anchors": [ "https://everyailaw.com/obligation-category/transparency.json" ], - "lifecycle_status": "unknown", - "operative_status": "unknown", - "enforcement_status": "unknown", + "lifecycle_status": "draft", + "operative_status": "future", + "enforcement_status": "unsignaled", "jurisdiction": { "@type": "of:Jurisdiction", "territorial_scope": [ diff --git a/scripts/lib/obligation-first.js b/scripts/lib/obligation-first.js index 8ea5ec59..553d2e95 100644 --- a/scripts/lib/obligation-first.js +++ b/scripts/lib/obligation-first.js @@ -347,7 +347,12 @@ function buildObligationRecords(config, data) { const detail = provisionDetails.get(mapping.id); const provision = detail ? detail.provision : {}; const recordId = concreteObligationId(mapping.id, obligationId); - const lifecycle = primary.lifecycle_status || provision.status || container?.status; + // A concrete obligation created only by a draft or proposed term is + // itself unissued: the term's state overrides the definition's + // generic lifecycle so a draft-derived duty never reads as operative. + const termStatus = provision.status || container?.status; + const draftCreated = ['draft', 'proposed'].includes(normalizeStatus(termStatus)); + const lifecycle = draftCreated ? termStatus : primary.lifecycle_status || termStatus; records.push(withEvidenceBoundary(compact({ '@context': recordContext(config), '@type': obligationType(primary.group), @@ -359,8 +364,8 @@ function buildObligationRecords(config, data) { applicability: provision.scope ? [`scope:${provision.scope}`] : undefined, anchors: sourceOwnedAnchors(mapping).obligationAnchors.length ? sourceOwnedAnchors(mapping).obligationAnchors : undefined, lifecycle_status: normalizeStatus(lifecycle), - operative_status: primary.operative_status || container?.operative_status || operativeStatus(lifecycle, force), - enforcement_status: primary.enforcement_status || container?.enforcement_status || enforcementStatus({ status: lifecycle, type: container?.type }, force), + operative_status: (draftCreated ? undefined : primary.operative_status) || container?.operative_status || operativeStatus(lifecycle, force), + enforcement_status: (draftCreated ? undefined : primary.enforcement_status) || container?.enforcement_status || enforcementStatus({ status: lifecycle, type: container?.type }, force), jurisdiction: typedJurisdiction(container?.jurisdiction), ...provenance(config, container?.official_url, provision.sections || mapping.source_heading, primary.last_verified || provision.verified || container?.last_verified, instrumentCitation(container || {})), 'pub:primary_id': obligationId, diff --git a/tests/fixtures/of-contract-fingerprint.json b/tests/fixtures/of-contract-fingerprint.json index f3faef7a..178f39e3 100644 --- a/tests/fixtures/of-contract-fingerprint.json +++ b/tests/fixtures/of-contract-fingerprint.json @@ -1894,9 +1894,9 @@ "present": 35 }, "enforcement_status": { - "explicit_unknown": 31, + "explicit_unknown": 30, "missing": 0, - "populated": 4, + "populated": 5, "present": 35 }, "evidence_type": { @@ -1910,15 +1910,15 @@ "present": 35 }, "lifecycle_status": { - "explicit_unknown": 28, + "explicit_unknown": 27, "missing": 0, - "populated": 7, + "populated": 8, "present": 35 }, "operative_status": { - "explicit_unknown": 31, + "explicit_unknown": 30, "missing": 0, - "populated": 4, + "populated": 5, "present": 35 }, "projection_basis": { diff --git a/tests/source-admission.test.js b/tests/source-admission.test.js index 6d8c1160..aee1e927 100644 --- a/tests/source-admission.test.js +++ b/tests/source-admission.test.js @@ -421,3 +421,32 @@ test('Obligation-First issuance determinations exclude original drafts even when const ids = JSON.parse(fs.readFileSync(path.join(ROOT, 'docs/api/v1/of/determinations.json'), 'utf8')).determinations.map(record => record['pub:id']); assert.equal(ids.includes('us-ut-oaip-jia-2026-001-issuance'), false); }); + +test('an obligation created only by a draft or proposed term keeps the draft state', () => { + const config = { url: 'https://publedge.org/' }; + const evidence = path => ({ kind: 'instrument', native_path: path, native_file_sha256: '1'.repeat(64), canonical_unit: null, canonical_sha256: '2'.repeat(64), admission_status: 'legacy-unreviewed', review_packet_sha256: null, retained_primary_sha256: null, unresolved_review_state: 'unknown', unresolved: null }); + // The shared definition declares an operative lifecycle; only the creating term decides whether this concrete duty is issued. + const primary = { id: 'example-duty', name: 'Example duty', group: 'requirement', lifecycle_status: 'operative', operative_status: 'operative', enforcement_status: 'enforced', _body: '## Summary\n\nExample.\n', _evidence_input: { ...evidence('data/examples/obligations/example-duty.md'), kind: 'obligation-definition' } }; + const project = container => { + const mapping = { id: 'example-mapping', regulation: container.id, obligations: ['example-duty'], source_heading: 'Summary', _evidence_input: { ...evidence('data/examples/mapping/index.yml'), kind: 'mapping-entry' } }; + return obligationFirst.buildObligationFirstRecords(config, { containers: [container], primaries: [primary], authorities: [], mappingIndex: [mapping] }).obligations[0]; + }; + const base = { id: 'us-ut-oaip-jia-2099-001', type: 'jia', jurisdiction: 'us-ut', authority: 'utah-oaip', _evidence_input: evidence(JIA_FILE) }; + + for (const status of ['draft', 'proposed']) { + const duty = project({ ...base, status }); + assert.equal(duty.lifecycle_status, status, `${status} creator must keep the duty ${status}`); + assert.notEqual(duty.operative_status, 'operative'); + assert.notEqual(duty.enforcement_status, 'enforced'); + } + + // Negative control: an issued creator leaves the definition's own state in place. + const issued = project({ ...base, type: 'rma', status: 'enforcing' }); + assert.ok(!['draft', 'proposed'].includes(issued.lifecycle_status)); + assert.equal(issued.operative_status, 'operative'); + assert.equal(issued.enforcement_status, 'enforced'); + + // The emitted JIA duty matches its draft term. + const emitted = JSON.parse(fs.readFileSync(path.join(ROOT, 'docs/api/v1/of/records/utah-mental-health-chatbot-disclosure-2026q2-first-session-disclose-genai-on-first-session.json'), 'utf8')); + assert.deepEqual([emitted.lifecycle_status, emitted.operative_status, emitted.enforcement_status], ['draft', 'future', 'unsignaled']); +});