Skip to content
Merged
5 changes: 5 additions & 0 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -4399,6 +4399,11 @@
"section": "archive",
"catalogued": true
},
{
"path": "docs/decisions/2026-10-17-sign-prose-only-source-reviews.md",
"section": "decisions",
"catalogued": true
},
{
"path": "docs/brand/psychsift-logo.md",
"section": "brand",
Expand Down
36 changes: 36 additions & 0 deletions docs/clinical-sign-off-how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,36 @@ npm run therapy:review -- --write --walk --reviewed-by "Dr <your full name>"

47 Therapy records list no references yet. The tool leaves them out, because the source correspondence question cannot be answered yes without a source; they stay awaiting review until one is adopted. Type `skip` at any question to move on to the next record without saving the one on screen.

## Signing off sources

Candidate clinical sources in the source acquisition ledger are signed with the same tool.
Sign one source by its id (the tool shows it), or walk the whole queue:

```bash
npm run clinical:review -- --write --kind source --code "<source id>" --reviewed-by "Dr <your full name>"
```

```bash
npm run clinical:review -- --write --walk --kind source --reviewed-by "Dr <your full name>"
```

The tool offers two kinds of source:

- **Unverified sources** that nobody has reviewed yet. Signing one marks it `locally_reviewed`.
- **Sources already marked `locally_reviewed` whose review was written down only in words**,
with no recorded signer, date or content pin ([owner decision, 2026-10-17](decisions/2026-10-17-sign-prose-only-source-reviews.md)). These show as
waiting in the sign-off ledger. Signing one records your name, the time and a pin of the
record's current content; its status stays `locally_reviewed`. Read the record on screen
again before you sign: you are vouching for it as it reads today, not as it read when it was
first reviewed.

A source marked `locally_reviewed` that already carries any part of a recorded sign-off (a
signer, a date or a content pin, even an empty one) is not offered this way. The one exception is
a complete sign-off (signer, date and pin all recorded) whose content has changed since: the tool
offers that for signing again. A partial or empty sign-off is never offered, even after the
content changes; it is a ledger error that `npm run check:source-acquisitions` reports. Signing a source never adopts it: whether clinical content cites it is a separate
decision. To send a source sign-off, add `src/data/source-acquisitions.json` in step 11 below.

## Indigenous content is never signed off here

Owner rule, 2026-09-26. Any record that mentions Aboriginal or Torres Strait Islander people,
Expand Down Expand Up @@ -352,6 +382,12 @@ a session:
git add src/data/formulation-concepts.json src/data/formulation-content.json
```

For sources:

```bash
git add src/data/source-acquisitions.json
```

For Therapy:

```bash
Expand Down
14 changes: 14 additions & 0 deletions docs/decisions/2026-10-17-sign-prose-only-source-reviews.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Decision: The owner can sign sources reviewed only in prose

- **Status:** decided 2026-10-17
- **Source:** `scripts/lib/signoff-kinds/sources.mjs`, find "The owner may sign it with this tool like a drafted source"
- **Retires:** `clinical-review-signs-only-unverified-sources`

`npm run clinical:review -- --kind source` now also offers a source that is marked `locally_reviewed`, is not
rejected, and has no attestation field at all, because its review was recorded only in prose. Signing it
writes attestedBy, attestedAt and attestedAgainstSha256 and leaves validationStatus as it is. Sources with
any attestation field, approved or rejected sources, and Indigenous content are still not offered this way.
The interactive-terminal requirement and the full-name signer rule are unchanged. The owner approved this
in chat on 17 Oct 2026 so they can sign the two OCP WA standards themselves.

If this summary and the source ever differ, the source wins.
8 changes: 8 additions & 0 deletions docs/organisation/retired-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@
"pattern": "does not run in this VM",
"allowedContext": "no longer|retired|historical|removed|replaced",
"decision": "docs/decisions/2026-10-17-cloud-installed-guidance-ownership.md"
},
{
"id": "clinical-review-signs-only-unverified-sources",
"retired": "2026-10-17",
"summary": "clinical:review --kind source no longer signs only unverified sources: a locally_reviewed source with no attestation field at all is offered too, so the owner can record the structured attestation it lacks.",
"pattern": "clinical:review (only signs|signs only) (unverified|drafted) sources|clinical:review only signs unverified",
"allowedContext": "no longer|retir(ed|ement)|removed|formerly|previously|superseded|dropped|replaced|historical",
"decision": "docs/decisions/2026-10-17-sign-prose-only-source-reviews.md"
}
]
}
6 changes: 5 additions & 1 deletion docs/source-acquisition-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,11 @@ A record may also carry `attestedBy`, `attestedAt` and `attestedAgainstSha256`.
optional, and all three are the owner's to write; an agent never records a sign-off on the owner's
behalf. The owner writes them with `npm run clinical:review -- --kind source` (one at a time with
`--walk`, or a whole review pack with `--pack` then `--write --batch`), which sets
`validationStatus` to `locally_reviewed` and computes the digest. Signing a source does not adopt
`validationStatus` to `locally_reviewed` and computes the digest. The tool also offers a source
already marked `locally_reviewed` whose review lives only in prose, meaning it has no
attestation field at all; signing it records the three fields and leaves `validationStatus` as
it is ([owner decision, 2026-10-17](decisions/2026-10-17-sign-prose-only-source-reviews.md)). A record with any attestation field, complete or partial,
is not offered this way. Signing a source does not adopt
it: `disposition` stays as it is until a separate adoption decision. Sources with Aboriginal,
Torres Strait Islander or other Indigenous content are never offered (owner rule 2026-09-26).

Expand Down
37 changes: 31 additions & 6 deletions scripts/lib/signoff-kinds/sources.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
* cannot carry them.
*
* Status: a record in `acquisitionReviewQueue()` (not rejected, still unverified) is
* `drafted`; one that is locally_reviewed or approved AND names its reviewer in attestedBy
* is `reviewed`; everything else (rejections, and reviews recorded only in prose) is
* `pending`. Walk order is the queue's: most local rung first, then title.
* `drafted`, and so is one marked locally_reviewed with no attestation field at all (a review
* recorded only in prose, which the owner can now sign properly; signing leaves its
* validationStatus as it is). One that is locally_reviewed or approved AND names its reviewer
* in attestedBy is `reviewed`; everything else (rejections, approved records reviewed only in
* prose, and any partial attestation) is `pending`. Walk order: most local rung first, then
* title.
*
* Signing a source does not adopt it. `disposition` stays `candidate` until clinical content
* is written to cite it, which is a separate, deliberate edit.
Expand Down Expand Up @@ -75,8 +78,25 @@ const presentText = (value) => (typeof value === "string" && value.trim() ? valu
/** What signing leaves in validationStatus; mirrors signedValidationStatus in the rating helper. */
const signedValidationStatus = (status) => (status === "unverified" ? "locally_reviewed" : status);

/**
* Marked locally_reviewed with the review recorded only in prose: no attestation field at all
* (not even an empty or null one). The owner may sign it with this tool like a drafted source,
* which records the structured attestation the ledger lacks (owner decision, 2026-10-17). A
* record carrying any attestation field, complete or partial, is never treated this way.
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
* Decision entry: docs/decisions/2026-10-17-sign-prose-only-source-reviews.md
*/
export function isUnattestedLocalReview(native) {
return (
isPlainRecord(native) &&
native.disposition !== "rejected" &&
native.validationStatus === "locally_reviewed" &&
ATTESTATION_FIELDS.every((key) => !Object.hasOwn(native, key))
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
);
}
Comment thread
BigSimmo marked this conversation as resolved.
Resolved

function sourceStatus(native) {
if (native.disposition !== "rejected" && native.validationStatus === "unverified") return "drafted";
if (isUnattestedLocalReview(native)) return "drafted";
if (REVIEWED_VALIDATION.has(native.validationStatus) && presentText(native.attestedBy)) return "reviewed";
return "pending";
}
Expand Down Expand Up @@ -292,9 +312,14 @@ export const display = {
...ratingRows(record, context),
[
"What signing does",
"Sets validationStatus to locally_reviewed and records your name, the time and a pin of every field above. " +
"It takes the source off the owner review queue and changes its rating as shown above. It does not adopt " +
"the source: disposition stays candidate until clinical content is written to cite it.",
record.validationStatus !== "unverified"
? `Records your name, the time and a pin of every field above. validationStatus is already ` +
`${record.validationStatus} and stays so, so its rating does not change; the source counts as signed ` +
"against its content as it reads now. It does not adopt the source: disposition stays as it is until " +
"clinical content is written to cite it."
: "Sets validationStatus to locally_reviewed and records your name, the time and a pin of every field above. " +
"It takes the source off the owner review queue and changes its rating as shown above. It does not adopt " +
"the source: disposition stays candidate until clinical content is written to cite it.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
],
];
},
Expand Down
119 changes: 99 additions & 20 deletions src/lib/developer-area/sign-off-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import { therapyNeedsReview, therapyRecords } from "@/lib/therapies";
import signableTherapySlugList from "@/data/therapy-signable-slugs.json";
import { isIndigenousContent, signedOffReviewer } from "@/lib/forms-reference-sign-off";
import {
ACQUISITION_ATTESTATION_FIELDS,
acquisitionAttestedContentSha256,
acquisitionReviewQueue,
sourceAcquisitionRecords,
type SourceAcquisitionRecord,
} from "@/lib/sources/acquisition-ledger";
import {
conceptReviewState,
Expand Down Expand Up @@ -516,29 +518,100 @@ function therapyFamily(): SignOffFamily {
};
}

const SOURCE_TITLE_COLLATOR = new Intl.Collator("en-AU");

/**
* Marked locally_reviewed, not rejected, with no attestation field at all: the review lives only
* in prose. Mirrors isUnattestedLocalReview in scripts/lib/signoff-kinds/sources.mjs, which is
* what lets clinical:review sign it.
*/
function isUnattestedLocalSourceReview(record: SourceAcquisitionRecord): boolean {
return (
record.disposition !== "rejected" &&
record.validationStatus === "locally_reviewed" &&
!ACQUISITION_ATTESTATION_FIELDS.some((field) => Object.hasOwn(record, field))
);
}

/**
* A reviewed source with a complete structured sign-off (all three attestation fields present)
* whose content changed after it was signed. A partial attestation is never treated as stale: it
* is a ledger error (acquisitionLedgerIssues) and stays awaiting prerequisites.
*/
function isStaleAttestedSourceReview(record: SourceAcquisitionRecord): boolean {
const complete = ACQUISITION_ATTESTATION_FIELDS.every((field) => {
const value = record[field];
return typeof value === "string" && value.trim().length > 0;
});
return (
record.disposition !== "rejected" &&
(record.validationStatus === "locally_reviewed" || record.validationStatus === "approved") &&
complete &&
record.attestedAgainstSha256 !== acquisitionAttestedContentSha256(record)
);
}

function sourcesFamily(): SignOffFamily {
// Reuses acquisitionReviewQueue rather than re-deriving the filter: that
// Reuses acquisitionReviewQueue rather than re-deriving its filter: that
// function already encodes which records count as awaiting sign-off (not
// rejected, still unverified) and the worst-rung-first order the source
// acquisition protocol reads them in.
const rows = acquisitionReviewQueue().map<SignOffRow>((record) => ({
family: "sources",
key: `source-acquisition:${record.id}`,
id: record.id,
title: record.title,
nativeStatus: `${record.disposition} / validationStatus: ${record.validationStatus} (rung ${record.rung})`,
statusLabel: "Captured, not yet verified",
requires:
"The owner verifies the source against the acquisition protocol before any clinical content may cite it; an adopted source with validationStatus unverified is already a ledger error.",
href: null,
signOff: toolUnlessIndigenous({ script: "clinical:review", kind: "source", code: record.id }, record),
}));
// Reviews recorded only in prose and stale attestations are offered by clinical:review too,
// so the owner can record or refresh the structured attestation. They are merged into the
// queue's own order (worst rung first, then title), as the tool walks them.
const ordered = [
...acquisitionReviewQueue(),
...sourceAcquisitionRecords.filter(
(record) => isUnattestedLocalSourceReview(record) || isStaleAttestedSourceReview(record),
),
].sort((left, right) => left.rung - right.rung || SOURCE_TITLE_COLLATOR.compare(left.title, right.title));
const rows = ordered.map<SignOffRow>((record) =>
isStaleAttestedSourceReview(record)
? {
family: "sources",
key: `source-acquisition:${record.id}`,
id: record.id,
title: record.title,
nativeStatus: `${record.disposition} / validationStatus: ${record.validationStatus} (rung ${record.rung}), stale attestation`,
statusLabel: "Edited since sign-off, needs signing again",
requires:
"The source changed after its last sign-off; the owner re-reads it as it reads today and signs it again.",
href: null,
signOff: toolUnlessIndigenous({ script: "clinical:review", kind: "source", code: record.id }, record),
}
: isUnattestedLocalSourceReview(record)
? {
family: "sources",
key: `source-acquisition:${record.id}`,
id: record.id,
title: record.title,
nativeStatus: `${record.disposition} / validationStatus: ${record.validationStatus} (rung ${record.rung}), no attestation`,
statusLabel: "Reviewed in prose only, not yet signed",
requires:
"The owner re-reads the source as it reads today and signs it, which records attestedBy, attestedAt and attestedAgainstSha256; validationStatus stays locally_reviewed.",
href: null,
signOff: toolUnlessIndigenous({ script: "clinical:review", kind: "source", code: record.id }, record),
}
: {
family: "sources",
key: `source-acquisition:${record.id}`,
id: record.id,
title: record.title,
nativeStatus: `${record.disposition} / validationStatus: ${record.validationStatus} (rung ${record.rung})`,
statusLabel: "Captured, not yet verified",
requires:
"The owner verifies the source against the acquisition protocol before any clinical content may cite it; an adopted source with validationStatus unverified is already a ledger error.",
href: null,
signOff: toolUnlessIndigenous({ script: "clinical:review", kind: "source", code: record.id }, record),
},
);
return {
id: "sources",
name: "Source acquisitions",
source: "src/data/source-acquisitions.json",
nativeField: "disposition + validationStatus, via acquisitionReviewQueue()",
note: "Worst rung first, the order the source acquisition protocol reads them in. Rejected records are excluded by that queue, not by this page.",
nativeField:
"disposition + validationStatus, via acquisitionReviewQueue(), plus prose-only reviews and stale attestations",
note: "Worst rung first, then title: the order the source acquisition protocol reads them in, with prose-only reviews and stale attestations merged in. Rejected records are excluded.",
unrouted: true,
rows,
};
Expand Down Expand Up @@ -1534,18 +1607,24 @@ function sourcesDomain(): SignOffDomainSummary {
reason: "Held for Aboriginal and Torres Strait Islander governance review.",
href: null,
});
} else if (record.validationStatus === "unverified") {
} else if (
record.validationStatus === "unverified" ||
isUnattestedLocalSourceReview(record) ||
isStaleAttestedSourceReview(record)
) {
// Unverified, locally_reviewed in prose only, or reviewed with a stale attestation:
// clinical:review offers all three for signing (scripts/lib/signoff-kinds/sources.mjs).
signableNow += 1;
} else {
// Reviewed only in prose: the source sign-off kind reads this as `pending`, and
// clinical:review signs drafted (unverified) sources only, so it is not signable now.
// Approved in prose only or a partial attestation: the source sign-off kind does
// not offer it, so it is not signable now.
prerequisiteRows.push({
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
domain: "sources",
domainName: "Source acquisitions",
key: `prereq-source:${record.id}`,
id: record.id,
title: record.title,
reason: `Marked ${record.validationStatus} without a structured attestation (attestedBy, attestedAt, attestedAgainstSha256 over its current content), so it does not count as signed; clinical:review only signs unverified sources.`,
reason: `Marked ${record.validationStatus} without a complete, current structured attestation (attestedBy, attestedAt, attestedAgainstSha256 over its current content), so it does not count as signed; clinical:review does not offer approved prose-only or partially attested sources.`,
href: null,
});
}
Expand All @@ -1562,7 +1641,7 @@ function sourcesDomain(): SignOffDomainSummary {
indigenousHoldRows,
awaitingPrerequisites: prerequisiteRows.length,
prerequisiteRows,
needsSigningSummary: `${signableNow} source(s) ready to sign now; ${indigenousHoldRows.length} Indigenous health and SEWB source(s) held for Aboriginal governance review; ${prerequisiteRows.length} reviewed only in prose and awaiting a structured attestation.`,
needsSigningSummary: `${signableNow} source(s) ready to sign now; ${indigenousHoldRows.length} Indigenous health and SEWB source(s) held for Aboriginal governance review; ${prerequisiteRows.length} awaiting a complete structured attestation or a supported review status.`,
queueFamilyId: "sources",
});
}
Expand Down
10 changes: 10 additions & 0 deletions src/lib/sources/acquisition-ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,16 @@ export function acquisitionLedgerIssues(
`${id}: an attestation must record attestedBy, attestedAt and attestedAgainstSha256; missing ${missing.join(", ")}`,
);
}
// An attestation field that is present but null or empty records no sign-off, yet it still
// keeps a prose-only review out of `clinical:review`, so name it rather than pass it silently.
const blank = ACQUISITION_ATTESTATION_FIELDS.filter(
(field) => Object.hasOwn(record, field) && presentText(record[field]) === null,
);
if (attestation.length === 0 && blank.length > 0) {
issues.push(
`${id}: ${blank.join(", ")} present but empty; remove the field, or record a complete attestation with clinical:review`,
);
}
// `npm run clinical:review -- --kind source` writes the exact UTC instant of the
// sign-off, the same rule every other sign-off kind holds; a bare date is still
// accepted for an attestation written by hand.
Expand Down
Loading
Loading