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
37 changes: 37 additions & 0 deletions real-time-collaborative-editor/comment-anchor-drift-gate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Comment Anchor Drift Gate

This slice supports issue #12, Real-time collaborative research editor &
interface. It reviews inline comments and annotations after document edits so
collaborative reviewers do not leave comments attached to the wrong sentence,
cell output, figure, table, or protocol step.

The module uses synthetic edit packets only. It does not connect to a live
editor, read private documents, or store collaborator identities.

## Checks

- Anchors whose quoted text no longer exists are blocked unless a new exact
anchor is supplied.
- Anchors that moved outside the expected section are blocked.
- Anchors attached to stale notebook output are blocked when execution evidence
changed.
- Large anchor movement requires reviewer confirmation.
- Clean anchors produce deterministic reviewer receipts.

## Local Verification

```bash
node real-time-collaborative-editor/comment-anchor-drift-gate/test.js
node real-time-collaborative-editor/comment-anchor-drift-gate/demo.js
git diff --check
```

The demo writes deterministic artifacts under
`real-time-collaborative-editor/comment-anchor-drift-gate/artifacts/`.

## Files

- `gate.js` - anchor drift checks and reviewer receipt generation.
- `data/anchor-packets.json` - synthetic collaborative edit packets.
- `test.js` - Node built-in tests.
- `demo.js` - deterministic JSON, Markdown, and SVG artifact generation.
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,108 @@
{
"summary": {
"total": 5,
"ready": 2,
"blocked": 3,
"findings": 4
},
"reports": [
{
"documentId": "doc-clean-methods",
"revisionId": "rev-001",
"decision": "ready",
"findings": [],
"receipt": {
"commentsReviewed": 1,
"blockedFindings": 0,
"heldFindings": 0,
"nextActions": []
}
},
{
"documentId": "doc-missing-anchor",
"revisionId": "rev-002",
"decision": "block",
"findings": [
{
"code": "missing_anchor_quote",
"severity": "block",
"target": "comment-2",
"message": "comment-2 quoted text no longer exists and no replacement anchor was provided.",
"action": "Re-anchor the comment to exact current text or mark it resolved."
}
],
"receipt": {
"commentsReviewed": 1,
"blockedFindings": 1,
"heldFindings": 0,
"nextActions": [
"Re-anchor the comment to exact current text or mark it resolved."
]
}
},
{
"documentId": "doc-section-drift",
"revisionId": "rev-003",
"decision": "block",
"findings": [
{
"code": "section_drift",
"severity": "block",
"target": "comment-3",
"message": "comment-3 moved from Limitations to Discussion.",
"action": "Confirm the comment still applies or re-anchor it in the expected section."
},
{
"code": "large_anchor_movement",
"severity": "hold",
"target": "comment-3",
"message": "comment-3 moved 220 characters, above the trusted movement threshold.",
"action": "Ask the reviewer to confirm or manually re-anchor the comment."
}
],
"receipt": {
"commentsReviewed": 1,
"blockedFindings": 1,
"heldFindings": 1,
"nextActions": [
"Confirm the comment still applies or re-anchor it in the expected section.",
"Ask the reviewer to confirm or manually re-anchor the comment."
]
}
},
{
"documentId": "doc-notebook-output",
"revisionId": "rev-004",
"decision": "block",
"findings": [
{
"code": "stale_notebook_output_anchor",
"severity": "block",
"target": "comment-4",
"message": "comment-4 targets notebook output whose execution record changed.",
"action": "Re-run output review and confirm the comment still applies to the new output."
}
],
"receipt": {
"commentsReviewed": 1,
"blockedFindings": 1,
"heldFindings": 0,
"nextActions": [
"Re-run output review and confirm the comment still applies to the new output."
]
}
},
{
"documentId": "doc-manual-confirmation",
"revisionId": "rev-005",
"decision": "ready",
"findings": [],
"receipt": {
"commentsReviewed": 1,
"blockedFindings": 0,
"heldFindings": 0,
"nextActions": []
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Comment Anchor Drift Gate

## Summary

- Anchor packets reviewed: 5
- Ready: 2
- Blocked: 3
- Findings: 4

## Reports

### doc-clean-methods

- Revision: rev-001
- Decision: ready
- Comments reviewed: 1

### doc-missing-anchor

- Revision: rev-002
- Decision: block
- Comments reviewed: 1
- block: missing_anchor_quote - comment-2 quoted text no longer exists and no replacement anchor was provided.

### doc-section-drift

- Revision: rev-003
- Decision: block
- Comments reviewed: 1
- block: section_drift - comment-3 moved from Limitations to Discussion.
- hold: large_anchor_movement - comment-3 moved 220 characters, above the trusted movement threshold.

### doc-notebook-output

- Revision: rev-004
- Decision: block
- Comments reviewed: 1
- block: stale_notebook_output_anchor - comment-4 targets notebook output whose execution record changed.

### doc-manual-confirmation

- Revision: rev-005
- Decision: ready
- Comments reviewed: 1

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
[
{
"documentId": "doc-clean-methods",
"revisionId": "rev-001",
"policy": { "maxTrustedOffsetDelta": 120 },
"sections": [
{ "id": "methods", "label": "Methods", "text": "Cells were seeded at 20,000 cells per well before treatment." }
],
"comments": [
{
"id": "comment-1",
"anchorType": "text",
"expectedSectionId": "methods",
"currentSectionId": "methods",
"quotedText": "20,000 cells per well",
"offsetDelta": 24,
"executionRecordChanged": false,
"reviewerConfirmed": false
}
]
},
{
"documentId": "doc-missing-anchor",
"revisionId": "rev-002",
"policy": { "maxTrustedOffsetDelta": 120 },
"sections": [
{ "id": "results", "label": "Results", "text": "The revised figure reports normalized response values." }
],
"comments": [
{
"id": "comment-2",
"anchorType": "text",
"expectedSectionId": "results",
"currentSectionId": "results",
"quotedText": "raw fluorescence increased threefold",
"offsetDelta": 18,
"executionRecordChanged": false,
"reviewerConfirmed": false
}
]
},
{
"documentId": "doc-section-drift",
"revisionId": "rev-003",
"policy": { "maxTrustedOffsetDelta": 120 },
"sections": [
{ "id": "discussion", "label": "Discussion", "text": "The limitation now appears in discussion text." },
{ "id": "limitations", "label": "Limitations", "text": "Sample size limitations remain important." }
],
"comments": [
{
"id": "comment-3",
"anchorType": "text",
"expectedSectionId": "limitations",
"currentSectionId": "discussion",
"quotedText": "limitation",
"offsetDelta": 220,
"executionRecordChanged": false,
"reviewerConfirmed": false
}
]
},
{
"documentId": "doc-notebook-output",
"revisionId": "rev-004",
"policy": { "maxTrustedOffsetDelta": 120 },
"sections": [
{ "id": "notebook-cell-7", "label": "Notebook cell 7", "text": "Mean response table output: 1.8, 2.1, 2.4." }
],
"comments": [
{
"id": "comment-4",
"anchorType": "notebook-output",
"expectedSectionId": "notebook-cell-7",
"currentSectionId": "notebook-cell-7",
"quotedText": "Mean response table output",
"offsetDelta": 42,
"executionRecordChanged": true,
"reviewerConfirmed": false
}
]
},
{
"documentId": "doc-manual-confirmation",
"revisionId": "rev-005",
"policy": { "maxTrustedOffsetDelta": 120 },
"sections": [
{ "id": "protocol", "label": "Protocol", "text": "The revised protocol includes the centrifuge speed and duration." }
],
"comments": [
{
"id": "comment-5",
"anchorType": "text",
"expectedSectionId": "protocol",
"currentSectionId": "protocol",
"quotedText": "centrifuge speed and duration",
"offsetDelta": 360,
"executionRecordChanged": false,
"reviewerConfirmed": true
}
]
}
]
69 changes: 69 additions & 0 deletions real-time-collaborative-editor/comment-anchor-drift-gate/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use strict";

const fs = require("node:fs");
const path = require("node:path");
const packets = require("./data/anchor-packets.json");
const { reviewAnchorPackets, summarizeReports } = require("./gate");

const artifactDir = path.join(__dirname, "artifacts");
const reports = reviewAnchorPackets(packets);
const summary = summarizeReports(reports);

fs.mkdirSync(artifactDir, { recursive: true });
fs.writeFileSync(path.join(artifactDir, "comment-anchor-drift-report.json"), `${JSON.stringify({ summary, reports }, null, 2)}\n`);
fs.writeFileSync(path.join(artifactDir, "comment-anchor-drift-report.md"), renderMarkdown(summary, reports));
fs.writeFileSync(path.join(artifactDir, "comment-anchor-drift-flow.svg"), renderSvg(summary));

console.log(`reviewed ${summary.total} anchor packets`);
console.log(`ready=${summary.ready} blocked=${summary.blocked} findings=${summary.findings}`);

function renderMarkdown(reportSummary, reportRows) {
const lines = [
"# Comment Anchor Drift Gate",
"",
"## Summary",
"",
`- Anchor packets reviewed: ${reportSummary.total}`,
`- Ready: ${reportSummary.ready}`,
`- Blocked: ${reportSummary.blocked}`,
`- Findings: ${reportSummary.findings}`,
"",
"## Reports",
"",
];

for (const report of reportRows) {
lines.push(`### ${report.documentId}`);
lines.push("");
lines.push(`- Revision: ${report.revisionId}`);
lines.push(`- Decision: ${report.decision}`);
lines.push(`- Comments reviewed: ${report.receipt.commentsReviewed}`);
for (const finding of report.findings) {
lines.push(` - ${finding.severity}: ${finding.code} - ${finding.message}`);
}
lines.push("");
}

return `${lines.join("\n")}\n`;
}

function renderSvg(reportSummary) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="860" height="250" role="img" aria-label="Comment anchor drift gate flow">
<rect width="860" height="250" fill="#111827"/>
<text x="32" y="44" fill="#f9fafb" font-family="Arial" font-size="22">Comment Anchor Drift Gate</text>
<g font-family="Arial" font-size="14" fill="#e5e7eb">
<rect x="34" y="88" width="190" height="76" rx="8" fill="#1f2937" stroke="#38bdf8"/>
<text x="58" y="118">Edit packet</text>
<text x="58" y="140">comments + sections</text>
<path d="M224 126 H296" stroke="#9ca3af" stroke-width="2"/>
<rect x="296" y="88" width="216" height="76" rx="8" fill="#1f2937" stroke="#a78bfa"/>
<text x="322" y="116">Quote, section, output,</text>
<text x="322" y="140">movement checks</text>
<path d="M512 126 H584" stroke="#9ca3af" stroke-width="2"/>
<rect x="584" y="88" width="238" height="76" rx="8" fill="#1f2937" stroke="#f97316"/>
<text x="612" y="116">Ready ${reportSummary.ready} / Blocked ${reportSummary.blocked}</text>
<text x="612" y="140">Findings ${reportSummary.findings}</text>
</g>
</svg>
`;
}
Loading