Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions collab-review-decision-ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Collaborative Review Decision Ledger

This is a focused Real-Time Collaborative Research Editor slice for SCIBASE issue #12. It evaluates whether a collaborative manuscript is ready to freeze or publish by checking unresolved review blockers across comments, suggestions, section locks, approvals, notebook freshness, decision records, and restore-safe snapshots.

## Scope

- Detects unresolved blocking comments.
- Detects open suggestions that must be accepted or rejected.
- Flags stale notebook outputs before release.
- Checks required role approvals for each section.
- Requires freeze-window section locks.
- Requires recorded sidebar decision records.
- Requires restore-ready snapshots before publish.
- Emits deterministic section decision digests and document-level priority actions.

It intentionally does not duplicate broad editor foundations, operation replay, offline conflict rebasing, notebook workbenches, reference formatting, authorship governance, lock/checkpoint recovery, freeze lanes, figure/table review lanes, discussion-sidebar audit, autosave recovery, or round-trip fidelity checks.

## Run

```powershell
node collab-review-decision-ledger/test.js
node collab-review-decision-ledger/demo.js
```

The demo writes:

- `collab-review-decision-ledger/demo-output/review-decision-ledger.json`
- `collab-review-decision-ledger/demo-output/demo.svg`

This PR also includes the required short MP4 demo artifact:

- `collab-review-decision-ledger/demo-output/demo.mp4`

## API

```js
const {
evaluateReviewDecision,
buildReleaseSummary,
createDecisionDigest,
} = require("./collab-review-decision-ledger");

const audit = evaluateReviewDecision({ manuscript, generatedAt });
```
27 changes: 27 additions & 0 deletions collab-review-decision-ledger/acceptance-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Acceptance Notes

## What This Adds

- Dependency-free Node.js module under `collab-review-decision-ledger/`.
- Deterministic section-level publish/freeze decisions for collaborative research manuscripts.
- Tests for unresolved release blockers, clean section readiness, document release summaries, and stable decision digests.
- Demo JSON, SVG, and MP4 artifacts for bounty review.

## Verification

Use these commands from the repository root:

```powershell
node collab-review-decision-ledger/test.js
node collab-review-decision-ledger/demo.js
node --check collab-review-decision-ledger/index.js
node --check collab-review-decision-ledger/test.js
node --check collab-review-decision-ledger/demo.js
node --check collab-review-decision-ledger/sample-data.js
ffprobe -v error -show_entries format=duration,size -show_entries stream=codec_name,width,height -of default=noprint_wrappers=1 collab-review-decision-ledger/demo-output/demo.mp4
git diff --check
```

## AI Assistance Disclosure

This contribution was prepared with AI assistance from OpenAI Codex and reviewed through local deterministic tests and artifact checks before submission.
Binary file not shown.
57 changes: 57 additions & 0 deletions collab-review-decision-ledger/demo-output/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"generatedAt": "2026-05-20T13:00:00.000Z",
"documentId": "doc-quantum-12",
"title": "Quantum catalyst manuscript",
"sectionDecisions": [
{
"sectionId": "intro",
"sectionTitle": "Introduction",
"generatedAt": "2026-05-20T13:00:00.000Z",
"decision": "ready_to_freeze",
"flags": [],
"riskScore": 0,
"blockingComments": [],
"openSuggestions": [],
"staleNotebooks": [],
"missingApprovals": [],
"releaseActions": [
"Keep section frozen for publish review"
],
"decisionDigest": "crdl_bf687c130eb59eeabd2c84e3"
},
{
"sectionId": "results",
"sectionTitle": "Results",
"generatedAt": "2026-05-20T13:00:00.000Z",
"decision": "hold_publish",
"flags": [
"BLOCKING_COMMENT_OPEN",
"OPEN_SUGGESTION",
"NOTEBOOK_OUTPUT_STALE",
"APPROVAL_MISSING",
"SECTION_UNLOCKED",
"DECISION_RECORD_MISSING",
"RESTORE_SNAPSHOT_UNSAFE"
],
"riskScore": 20,
"blockingComments": [
"c-7"
],
"openSuggestions": [
"sg-7"
],
"staleNotebooks": [
"nb-7"
],
"missingApprovals": [
"stat-reviewer"
],
"releaseActions": [
"Resolve 1 blocking comment in Results",
"Accept or reject 1 open suggestion in Results",
"Refresh 1 stale notebook output before freeze",
"Collect approval from stat-reviewer",
"Move Results into freeze-window lock mode",
"Create restore-ready snapshot for Results"
],
"decisionDigest": "crdl_58fa90d9db1ded935bd59ca8"
},
{
"sectionId": "methods",
"sectionTitle": "Methods",
"generatedAt": "2026-05-20T13:00:00.000Z",
"decision": "ready_to_freeze",
"flags": [],
"riskScore": 0,
"blockingComments": [],
"openSuggestions": [],
"staleNotebooks": [],
"missingApprovals": [],
"releaseActions": [
"Keep section frozen for publish review"
],
"decisionDigest": "crdl_ed49826af04eabfc3f989582"
}
],
"releaseSummary": {
"counts": {
"sections": 3,
"readyToFreeze": 2,
"holdPublish": 1,
"blockingComments": 1,
"staleNotebooks": 1,
"missingApprovals": 1
},
"priorityActions": [
"Hold publish: 1 section has unresolved release blockers.",
"Resolve 1 blocking comment before freeze.",
"Refresh 1 stale notebook output before release.",
"Collect 1 missing role approval before publish."
]
}
}
80 changes: 80 additions & 0 deletions collab-review-decision-ledger/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const fs = require("fs");
const path = require("path");

const { evaluateReviewDecision } = require("./index");
const { manuscript } = require("./sample-data");

const generatedAt = "2026-05-20T13:00:00.000Z";
const outputDir = path.join(__dirname, "demo-output");

fs.mkdirSync(outputDir, { recursive: true });

const audit = evaluateReviewDecision({ manuscript, generatedAt });
fs.writeFileSync(path.join(outputDir, "review-decision-ledger.json"), `${JSON.stringify(audit, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "demo.svg"), buildSvg(audit));

console.log("Collaborative review decision ledger demo");
console.log(`Document: ${audit.title}`);
console.log(`Sections: ${audit.releaseSummary.counts.sections}`);
console.log(`Ready to freeze: ${audit.releaseSummary.counts.readyToFreeze}`);
console.log(`Hold publish: ${audit.releaseSummary.counts.holdPublish}`);
console.log(`Blocking comments: ${audit.releaseSummary.counts.blockingComments}`);
console.log(`Wrote ${path.join(outputDir, "review-decision-ledger.json")}`);
console.log(`Wrote ${path.join(outputDir, "demo.svg")}`);

function buildSvg(audit) {
const rows = audit.sectionDecisions.map((section, index) => {
const y = 196 + index * 82;
const color = section.decision === "ready_to_freeze" ? "#1f8a5b" : "#b42318";
const flags = section.flags.length === 0 ? "No blockers" : section.flags.join(" | ");
return `<g transform="translate(64 ${y})">
<rect width="832" height="60" rx="8" fill="#ffffff" stroke="#cfd8dc"/>
<circle cx="26" cy="30" r="10" fill="${color}"/>
<text x="48" y="25" class="team">${escapeXml(section.sectionTitle)}</text>
<text x="48" y="45" class="meta">${escapeXml(formatDecision(section.decision))} - risk ${section.riskScore}</text>
<text x="330" y="25" class="small">${escapeXml(flags)}</text>
<text x="330" y="45" class="small">${escapeXml(section.decisionDigest)}</text>
</g>`;
}).join("");

return `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="520" viewBox="0 0 960 520" role="img" aria-label="Collaborative review decision ledger demo">
<style>
.title { font: 700 28px Arial, sans-serif; fill: #102027; }
.subtitle { font: 400 15px Arial, sans-serif; fill: #455a64; }
.metric { font: 700 24px Arial, sans-serif; }
.label { font: 400 13px Arial, sans-serif; fill: #546e7a; }
.team { font: 700 16px Arial, sans-serif; fill: #102027; }
.meta { font: 400 13px Arial, sans-serif; fill: #455a64; }
.small { font: 400 11px Arial, sans-serif; fill: #607d8b; }
</style>
<rect width="960" height="520" fill="#f4f7f9"/>
<text x="64" y="58" class="title">Collaborative Review Decision Ledger</text>
<text x="64" y="84" class="subtitle">${escapeXml(audit.title)} - ${escapeXml(audit.generatedAt)}</text>
${metricCard(64, 112, "Sections", audit.releaseSummary.counts.sections, "#0b5fff")}
${metricCard(252, 112, "Ready", audit.releaseSummary.counts.readyToFreeze, "#1f8a5b")}
${metricCard(440, 112, "Hold", audit.releaseSummary.counts.holdPublish, "#b42318")}
${metricCard(628, 112, "Comments", audit.releaseSummary.counts.blockingComments, "#ad6f00")}
${rows}
<text x="64" y="478" class="subtitle">${escapeXml(audit.releaseSummary.priorityActions.join(" "))}</text>
</svg>`;
}

function metricCard(x, y, label, value, color) {
return `<g transform="translate(${x} ${y})">
<rect width="150" height="58" rx="8" fill="#ffffff" stroke="#cfd8dc"/>
<text x="18" y="25" class="metric" fill="${color}">${value}</text>
<text x="18" y="45" class="label">${escapeXml(label)}</text>
</g>`;
}

function formatDecision(decision) {
return decision.split("_").map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
}

function escapeXml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
Loading