diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a726b2af4..afe5a8e1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -115,6 +115,7 @@ jobs: packages/ui/utils/diagramAnchorGraphviz.test.ts packages/ui/hooks/useAnnotationHighlighter.diagramSkip.test.tsx packages/ui/components/Viewer.diagramLazyRestore.test.tsx + packages/ui/components/Viewer.diagramAnchorHostile.test.tsx packages/ui/components/CommentPopover.skillReferences.test.tsx packages/ui/components/SkillReferenceMenu.placement.test.tsx packages/ui/components/sidebar/FileBrowser.test.ts diff --git a/AGENTS.md b/AGENTS.md index a5526d782..06af20e5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -600,7 +600,7 @@ During normal plan review, an Archive sidebar tab provides the same browsing via | `/api/external-annotations/stream` | GET | SSE stream for real-time external annotations | | `/api/external-annotations` | GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) | | `/api/external-annotations` | POST | Add external annotations (single or batch `{ annotations: [...] }`) | -| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`) | +| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`). The body is allowlisted and field-validated by `validateAnnotationPatch` (`@plannotator/core/external-annotation`, both runtimes) with the SAME validators POST applies — `diagramAnchor` / `htmlAnchor` / `elementContext` / the target arrays through their own fail-closed parsers, `inReplyTo` through `validateReplyTarget`, the scalars by type and cap. A bad value is `400`, unknown keys are dropped, `id` and `source` stay immutable, and `null` clears an optional field but is refused on an anchor or a structural one | | `/api/external-annotations` | DELETE | Remove by `?id=`, `?source=`, or clear all | ### Review Server (`packages/server/review.ts`) @@ -634,7 +634,7 @@ During normal plan review, an Archive sidebar tab provides the same browsing via | `/api/external-annotations/stream` | GET | SSE stream for real-time external annotations | | `/api/external-annotations` | GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) | | `/api/external-annotations` | POST | Add external annotations (single or batch `{ annotations: [...] }`) | -| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`) | +| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`). The body is allowlisted and field-validated by `validateAnnotationPatch` (`@plannotator/core/external-annotation`, both runtimes) with the SAME validators POST applies — `diagramAnchor` / `htmlAnchor` / `elementContext` / the target arrays through their own fail-closed parsers, `inReplyTo` through `validateReplyTarget`, the scalars by type and cap. A bad value is `400`, unknown keys are dropped, `id` and `source` stay immutable, and `null` clears an optional field but is refused on an anchor or a structural one | | `/api/external-annotations` | DELETE | Remove by `?id=`, `?source=`, or clear all | | `/api/agents/capabilities` | GET | Check available agent providers (claude, codex, tour, guide, cursor, opencode, pi, copilot) | | `/api/agents/review-profiles` | GET | List launchable review profiles (enabled skills + builtin default) | @@ -696,7 +696,7 @@ During normal plan review, an Archive sidebar tab provides the same browsing via | `/api/external-annotations/stream` | GET | SSE stream for real-time external annotations | | `/api/external-annotations` | GET | Snapshot of external annotations (polling fallback, `?since=N` for version gating) | | `/api/external-annotations` | POST | Add external annotations (single or batch `{ annotations: [...] }`) | -| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`) | +| `/api/external-annotations` | PATCH | Update fields on a single annotation (`?id=`). The body is allowlisted and field-validated by `validateAnnotationPatch` (`@plannotator/core/external-annotation`, both runtimes) with the SAME validators POST applies — `diagramAnchor` / `htmlAnchor` / `elementContext` / the target arrays through their own fail-closed parsers, `inReplyTo` through `validateReplyTarget`, the scalars by type and cap. A bad value is `400`, unknown keys are dropped, `id` and `source` stay immutable, and `null` clears an optional field but is refused on an anchor or a structural one | | `/api/external-annotations` | DELETE | Remove by `?id=`, `?source=`, or clear all | All servers use random ports locally or fixed port (`19432`) in remote mode. @@ -966,7 +966,7 @@ Text highlighting uses `web-highlighter` library. Code blocks use manual ` *Counts:* the panel header shows the scoped count, but the session total the decision control runs on (`feedbackAnnotationCount`) is computed from session state and is unaffected by the toggle — switching scope never changes what is submitted. Not covered: code review, single-document plan review, and live-app multi-page sessions (which already group their export by page). The compact/touch sheet reuses `AnnotationPanel`, so it gets the same toggle in its own header bar. Every panel prop is optional and additive, so a host that passes none of them (Workspaces) renders the previous panel, legacy affordance included. -**Diagram comments (Mermaid and Graphviz fences).** A comment composed on a rendered diagram part — a click on a node, edge or cluster in the inline canvas or the popout — becomes an `Annotation` on the document with ONE additive field, `diagramAnchor` (`DiagramAnchor` from `@plannotator/core/diagram-anchor`, the `htmlAnchor` precedent): `{ v: 1, family, kind, id | from + to, label, sourceLine }` (families: flowchart, state, class, er, requirement, sequence, other, graphviz; kinds: node, edge, cluster, and `diagram` for the whole diagram, which has no id), where the anchor is the diagram's OWN id for the part (sequence parts carry classes, not ids, so the codec's ids are the actor's `name`, `msg-`, `note-`, `frame-` by document order, restored with a label check because an ordinal moves) (never the rendered element id with its trailing counter, never geometry) and `sourceLine` names 1-based DOCUMENT lines (the fence's opening line is the offset, `Block.startLine`). The annotation carries the fence's `blockId`, `startOffset`/`endOffset` 0, the label as `originalText`, `type: COMMENT` and the tater identity, so it lists in the annotations rail beside text comments, exports, persists in drafts (opaque JSON), and restores after a reload: `useAnnotationHighlighter` skips rows carrying `diagramAnchor` (neither attempted nor unanchored), and `DiagramBlock` re-resolves them against every render through the engine's finder — id, then label, then unanchored — and reports through `Viewer.onRestoreReport` with its own comments as `attempted`, so App's `markdownUnanchoredIds` shows the same "Unanchored" chip a text comment gets. All three export paths (`exportAnnotations`, `exportLinkedDocAnnotations`, `exportAnnotationEntry`) print the location line `Diagram node