@dailephd/my-frontend-observer 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +107 -7
  3. package/dist/application/projectWorkflowService.d.ts +18 -1
  4. package/dist/application/projectWorkflowService.js +40 -2
  5. package/dist/application/projectWorkflowService.js.map +1 -1
  6. package/dist/application/visualAnnotationContractPromotionService.d.ts +41 -0
  7. package/dist/application/visualAnnotationContractPromotionService.js +143 -0
  8. package/dist/application/visualAnnotationContractPromotionService.js.map +1 -0
  9. package/dist/application/visualAnnotationPersistenceService.d.ts +34 -0
  10. package/dist/application/visualAnnotationPersistenceService.js +68 -0
  11. package/dist/application/visualAnnotationPersistenceService.js.map +1 -0
  12. package/dist/application/visualAnnotationReferenceMaterializationService.d.ts +53 -0
  13. package/dist/application/visualAnnotationReferenceMaterializationService.js +194 -0
  14. package/dist/application/visualAnnotationReferenceMaterializationService.js.map +1 -0
  15. package/dist/artifacts/visualAnnotationArtifactReader.d.ts +19 -0
  16. package/dist/artifacts/visualAnnotationArtifactReader.js +66 -0
  17. package/dist/artifacts/visualAnnotationArtifactReader.js.map +1 -0
  18. package/dist/artifacts/visualAnnotationArtifactWriter.d.ts +42 -0
  19. package/dist/artifacts/visualAnnotationArtifactWriter.js +85 -0
  20. package/dist/artifacts/visualAnnotationArtifactWriter.js.map +1 -0
  21. package/dist/cli.js +464 -454
  22. package/dist/cli.js.map +1 -1
  23. package/dist/domain/visualAnnotation.d.ts +217 -0
  24. package/dist/domain/visualAnnotation.js +584 -0
  25. package/dist/domain/visualAnnotation.js.map +1 -0
  26. package/dist/domain/visualAnnotationIdentity.d.ts +17 -0
  27. package/dist/domain/visualAnnotationIdentity.js +47 -0
  28. package/dist/domain/visualAnnotationIdentity.js.map +1 -0
  29. package/dist/index.d.ts +11 -0
  30. package/dist/index.js +6 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/projectWorkflow/projectPaths.d.ts +9 -0
  33. package/dist/projectWorkflow/projectPaths.js +21 -0
  34. package/dist/projectWorkflow/projectPaths.js.map +1 -1
  35. package/dist/viewer/assets/{index-CN_yb9Uf.css → index-BN41MI7m.css} +1 -1
  36. package/dist/viewer/assets/index-CkKXnlrI.js +9 -0
  37. package/dist/viewer/index.html +2 -2
  38. package/dist/viewer/sw.js +1 -1
  39. package/dist/viewerServer/annotationAuthoring.d.ts +64 -0
  40. package/dist/viewerServer/annotationAuthoring.js +230 -0
  41. package/dist/viewerServer/annotationAuthoring.js.map +1 -0
  42. package/dist/viewerServer/annotationContractPromotion.d.ts +51 -0
  43. package/dist/viewerServer/annotationContractPromotion.js +105 -0
  44. package/dist/viewerServer/annotationContractPromotion.js.map +1 -0
  45. package/dist/viewerServer/annotationReferenceMaterialization.d.ts +40 -0
  46. package/dist/viewerServer/annotationReferenceMaterialization.js +91 -0
  47. package/dist/viewerServer/annotationReferenceMaterialization.js.map +1 -0
  48. package/dist/viewerServer/authoringSecurity.d.ts +59 -0
  49. package/dist/viewerServer/authoringSecurity.js +112 -0
  50. package/dist/viewerServer/authoringSecurity.js.map +1 -0
  51. package/dist/viewerServer/evidence/annotationView.d.ts +28 -0
  52. package/dist/viewerServer/evidence/annotationView.js +43 -0
  53. package/dist/viewerServer/evidence/annotationView.js.map +1 -0
  54. package/dist/viewerServer/evidence/classify.d.ts +3 -1
  55. package/dist/viewerServer/evidence/classify.js +12 -0
  56. package/dist/viewerServer/evidence/classify.js.map +1 -1
  57. package/dist/viewerServer/evidence/discovery.d.ts +2 -0
  58. package/dist/viewerServer/evidence/discovery.js +6 -0
  59. package/dist/viewerServer/evidence/discovery.js.map +1 -1
  60. package/dist/viewerServer/evidence/handles.js +1 -0
  61. package/dist/viewerServer/evidence/handles.js.map +1 -1
  62. package/dist/viewerServer/evidence/index.d.ts +29 -0
  63. package/dist/viewerServer/evidence/index.js +43 -1
  64. package/dist/viewerServer/evidence/index.js.map +1 -1
  65. package/dist/viewerServer/evidence/mediaResolver.d.ts +1 -1
  66. package/dist/viewerServer/evidence/mediaResolver.js +28 -2
  67. package/dist/viewerServer/evidence/mediaResolver.js.map +1 -1
  68. package/dist/viewerServer/evidence/projection.d.ts +5 -1
  69. package/dist/viewerServer/evidence/projection.js +19 -0
  70. package/dist/viewerServer/evidence/projection.js.map +1 -1
  71. package/dist/viewerServer/httpServer.d.ts +13 -2
  72. package/dist/viewerServer/httpServer.js +278 -4
  73. package/dist/viewerServer/httpServer.js.map +1 -1
  74. package/dist/viewerServer/viewerService.d.ts +8 -0
  75. package/dist/viewerServer/viewerService.js +38 -2
  76. package/dist/viewerServer/viewerService.js.map +1 -1
  77. package/docs/ARCHITECTURE.md +108 -21
  78. package/docs/CI_CD.md +57 -1
  79. package/docs/COMMANDS.md +44 -4
  80. package/docs/CONTRACTS.md +78 -8
  81. package/docs/CURRENT_STATE.md +157 -35
  82. package/docs/DEVELOPMENT.md +8 -3
  83. package/docs/PROJECT_DESCRIPTION.md +4 -1
  84. package/docs/PROJECT_MILESTONES.md +4 -0
  85. package/docs/PROJECT_OVERVIEW.md +41 -17
  86. package/docs/QUICKSTART.md +52 -39
  87. package/docs/RELEASE.md +16 -11
  88. package/docs/ROADMAP.md +406 -66
  89. package/docs/SECURITY.md +71 -14
  90. package/docs/WORKFLOWS.md +151 -23
  91. package/docs/plans/v0.9-implementation-plan.md +1529 -0
  92. package/docs/reports/v0.9-architecture-retrieval.md +567 -0
  93. package/docs/reports/v0.9-batch1-visual-annotation-foundation.md +351 -0
  94. package/docs/reports/v0.9-batch2-viewer-annotation-authoring-boundary.md +438 -0
  95. package/docs/reports/v0.9-batch3-runtime-screenshot-annotation-authoring.md +412 -0
  96. package/docs/reports/v0.9-batch4-external-reference-annotation-authoring.md +452 -0
  97. package/docs/reports/v0.9-batch5-runtime-intent-contract-promotion.md +535 -0
  98. package/docs/reports/v0.9-batch6-reference-materialization.md +514 -0
  99. package/docs/reports/v0.9-batch7-integrated-acceptance.md +644 -0
  100. package/docs/reports/v0.9-demo-foundation.md +589 -0
  101. package/docs/reports/v0.9-final-pre-release-readiness.md +209 -0
  102. package/docs/reports/v0.9-final-readiness-corrections.md +530 -0
  103. package/docs/reports/v0.9-pre-release-readiness.md +170 -0
  104. package/docs/reports/v0.9-tutorial-end-to-end-acceptance.md +980 -0
  105. package/docs/reports/v0.9-tutorial-integration.md +731 -0
  106. package/package.json +2 -2
  107. package/dist/viewer/assets/index-D98S1_2d.js +0 -9
@@ -0,0 +1,412 @@
1
+ # v0.9 Batch 3 Report: Runtime Screenshot Annotation Authoring
2
+
3
+ ## 1. VERDICT
4
+
5
+ PASS_V0_9_BATCH3_RUNTIME_SCREENSHOT_ANNOTATION_AUTHORING
6
+
7
+ ## 2. Repository identity
8
+
9
+ 1. Repository: `C:\Users\daile\Projects\my-frontend-observer`
10
+ 2. Branch: `master`
11
+ 3. Starting HEAD: `3776c356d1c7c8848c633d0a3c0fab46501b9a04`
12
+ 4. Ending HEAD before the final commit: `3776c356d1c7c8848c633d0a3c0fab46501b9a04`
13
+ 5. Package version: `0.8.1` (unchanged in `package.json` and `package-lock.json`)
14
+
15
+ ## 3. Prompt 2 entry gate
16
+
17
+ 1. Prompt 2 verdict: `PASS_V0_9_BATCH2_VIEWER_ANNOTATION_AUTHORING_BOUNDARY`
18
+ 2. Prompt 2 commit: `3776c356d1c7c8848c633d0a3c0fab46501b9a04`
19
+ 3. Viewer protocol: `1.1.0`
20
+ 4. Annotation save route: `POST /api/annotations`
21
+ 5. Project-aware authoring: enabled
22
+ 6. Standalone viewer: read-only
23
+ 7. Preflight found a clean tracked worktree on `master` at the required HEAD.
24
+ 8. `git ls-files -v docs/reports/v0.9-architecture-retrieval.md` printed `S`
25
+ at entry and again before commit. The `skip-worktree` state was preserved.
26
+ The file was not restored, overwritten, or committed. No pull, rebase,
27
+ reset, or clean was run.
28
+ 9. No Prompt 2 API defect was found. The UI uses the Prompt 2 API unchanged.
29
+
30
+ ## 4. Authoring-session consumption
31
+
32
+ 1. `viewer/src/hooks/useAuthoringSession.ts` fetches
33
+ `GET /api/authoring/session` with `cache: 'no-store'` when the observation
34
+ workspace mounts.
35
+ 2. States: `loading`, `read-only`, `available` (with the token), and `error`.
36
+ 3. The token lives only in React state. It is never written to localStorage,
37
+ sessionStorage, IndexedDB, a URL, a log, a service-worker message, or the
38
+ visible UI. The only place it leaves memory is the save request header.
39
+ A source search of the new viewer files found no storage APIs and no
40
+ `console` calls.
41
+ 4. A read-only viewer is a normal state:
42
+ 1. The toolbar shows only Select and Pan plus a "Read-only viewer" notice.
43
+ 2. The panel can still list and display saved annotations for inspection.
44
+ 3. No drawing, editing, or save controls are shown.
45
+
46
+ ## 5. Runtime annotation discovery
47
+
48
+ `viewer/src/hooks/useRuntimeAnnotations.ts` uses only existing APIs.
49
+
50
+ 1. `GET /api/index` gives the candidates: `family === 'visual-annotation'`,
51
+ `supportState === 'supported'`, and
52
+ `relatedIds.sourceObservationId === observationId`. At most 100 candidates
53
+ are checked, and truncation is reported.
54
+ 2. `GET /api/annotations/:handle/view` confirms each candidate. The pure
55
+ helper `annotationViewBelongsToObservation` requires `source.status`
56
+ `available`, `source.family` `observation`, and `source.handle` equal to
57
+ the selected observation handle.
58
+ 3. Aliases, relative directories, and labels are never used to join an
59
+ annotation to an observation.
60
+
61
+ The panel's saved list shows, for each annotation:
62
+
63
+ 1. `annotationId`
64
+ 2. item count
65
+ 3. confirmed item count
66
+ 4. whether it is a revision
67
+
68
+ No paths are shown. Selecting a saved annotation copies its items into the
69
+ in-memory draft. It sets that annotation as the parent handle and keeps every
70
+ `annotationItemId`. The persisted artifact is never mutated.
71
+
72
+ ## 6. Shared SVG source-coordinate transform
73
+
74
+ 1. New `viewer/src/svg/sourceCoordinates.ts` provides
75
+ `screenPointToSvgSource(svg, clientX, clientY)`. It uses
76
+ `getScreenCTM()`, `createSVGPoint()`, `ctm.inverse()`, and
77
+ `matrixTransform()`.
78
+ 2. `viewer/src/hooks/useZoomPan.ts` now calls this helper. Its private
79
+ duplicate was removed.
80
+ 3. Annotation drawing and moving (`useAnnotationPointerInteraction.ts`) call
81
+ the same helper.
82
+ 4. A search of `viewer/src` finds exactly one `getScreenCTM()` or
83
+ `matrixTransform` implementation. No bounding-rectangle ratios,
84
+ devicePixelRatio, CSS ratios, or manual aspect-ratio math are used.
85
+ 5. The source frame is `artifact.requestConfig.viewport` (runtime CSS pixels).
86
+ Coordinates are never rounded, clamped, or scaled. A pointer position
87
+ outside `0..width` by `0..height` is ignored. A gesture keeps its last
88
+ valid in-frame position.
89
+
90
+ ## 7. Interaction modes
91
+
92
+ 1. `AnnotationInteractionMode` is the closed set `select`, `pan`, `point`,
93
+ `rectangle`, `line`, `arrow`, `note`. It is defined in the new pure module
94
+ `viewer/src/annotation/annotationGeometry.ts`.
95
+ 2. `useAnnotationPointerInteraction` attaches exactly one handler set to the
96
+ SVG at a time:
97
+ 1. `pan`: the existing `useZoomPan` pointer handlers only. Target and mark
98
+ pointer input is disabled.
99
+ 2. Drawing modes: drawing handlers only. Target rectangles get
100
+ `pointer-events: none`, and marks are not selectable, so drawing over a
101
+ target never selects it and never moves marks.
102
+ 3. `select`: target selection, mark selection, and mark drag-to-move. The
103
+ background neither pans nor draws.
104
+ 3. A drag therefore can never mean both pan and draw.
105
+
106
+ ## 8. Annotation layer
107
+
108
+ 1. New `viewer/src/components/AnnotationLayer.tsx` renders one `<g>` inside the
109
+ existing `TargetOverlaySvg`. It comes after the screenshot, relationships,
110
+ and targets, so annotations render on top. There is no second SVG, no
111
+ canvas, and no positioned DOM layer.
112
+ 2. It receives only items, the selected item id, a selectable flag, callbacks,
113
+ and an optional preview mark. It knows nothing about artifacts, paths,
114
+ tokens, the API, or aliases, so Prompt 4 can reuse it.
115
+ 3. It renders the five mark kinds with fixed classes. Note text is plain React
116
+ text: no `dangerouslySetInnerHTML` and no authored SVG.
117
+ 4. Each mark has `data-annotation-item-id`, `data-annotation-kind`, and a
118
+ bounded `aria-label`.
119
+ 5. `TargetOverlaySvg` gains three optional props: `annotationLayer`,
120
+ `targetsInteractive` (default `true`), and `interactionClassName`.
121
+ Screenshot, targets, labels, relationship connectors, target selection, and
122
+ viewBox behavior are otherwise unchanged. Comparison and reference
123
+ workspaces pass none of the new props.
124
+
125
+ ## 9. Draft lifecycle
126
+
127
+ `viewer/src/hooks/useRuntimeAnnotationDraft.ts` owns the draft.
128
+
129
+ 1. New annotation: no items, no parent, nothing selected, not dirty.
130
+ 2. Drawing:
131
+ 1. Point and note are created on pointer release at the pointer-down
132
+ source point.
133
+ 2. Rectangle, line, and arrow show a dashed preview while dragging and are
134
+ created on release.
135
+ 3. Rectangles are direction-normalized. A zero-width or zero-height
136
+ rectangle is not created.
137
+ 4. Local choice: a zero-length line or arrow is not created either.
138
+ 5. A note is created only when the toolbar note text is 1 to 2000
139
+ characters.
140
+ 3. New item: `annotation-item-${crypto.randomUUID()}`, no association,
141
+ `interpretation: { state: 'uninterpreted' }`. It is selected on creation
142
+ and marks the draft dirty.
143
+ 4. Move (select mode): drag a mark past a 3-pixel threshold. It translates
144
+ rigidly by the source delta through the shared transform. A translation
145
+ that would leave the frame is not applied. The id, association, and
146
+ interpretation are unchanged. There are no resize handles.
147
+ 5. Note edit: a controlled textarea for the selected note changes only
148
+ `mark.text`, bounded at 2000 characters. Saving is disabled while any note
149
+ is empty.
150
+ 6. Delete draft item removes the item from memory only. No HTTP DELETE is
151
+ sent.
152
+ 7. Cancel changes restores the loaded persisted items, or an empty draft for a
153
+ new annotation. It also clears transient drawing and any save error.
154
+ 8. Loaded items keep their persisted interpretation. No interpretation UI
155
+ exists in this batch.
156
+
157
+ ## 10. Target association
158
+
159
+ 1. Drawing never associates anything.
160
+ 2. With a draft item selected and a runtime target selected (from the target
161
+ list or SVG), the explicit "Associate target <name>" button sets
162
+ `{ kind: 'runtime-target', target }`.
163
+ 3. "Clear association" removes it.
164
+ 4. Annotation selection and target selection are separate state.
165
+ 5. The display reads "Target association: <name>" or "No association". This
166
+ wording is deliberately different from the inspector's "Target: <name>".
167
+
168
+ ## 11. Relationship association
169
+
170
+ 1. Options come only from the server graph already fetched from
171
+ `/api/observations/:handle/relationships`.
172
+ 1. Pairwise options keep `relationshipKind`, `subjectTarget`, and
173
+ `relatedTarget`.
174
+ 2. Page-level options keep `relationshipKind` only.
175
+ 2. The user must choose an option and press "Associate relationship". Lines
176
+ and arrows never imply a relationship.
177
+ 3. The display reads "Relationship association: <kind> (<subject> →
178
+ <related>)".
179
+
180
+ ## 12. Zoom/pan behavior
181
+
182
+ 1. The observation workspace now uses
183
+ `useZoomPan(viewport.width, viewport.height)` with the existing
184
+ `ZoomControls`: Zoom in, Zoom out, Fit, and Reset. Reset equals Fit.
185
+ 2. Bounds are unchanged (`ZOOM_MIN = 1`, `ZOOM_MAX = 8`, `ZOOM_STEP = 1.25`).
186
+ 3. Zoom and pan change only the SVG viewBox. Target and annotation geometry
187
+ and the screenshot frame are unchanged. Zoom state is never persisted.
188
+
189
+ ## 13. Save/reload behavior
190
+
191
+ 1. Save runs only when the session is `available`. It POSTs
192
+ `{ sourceHandle, parentAnnotationHandle?, items }` with `content-type` and
193
+ the token header. The browser supplies Origin.
194
+ 2. On 201, the UI:
195
+ 1. Reads the returned handle.
196
+ 2. Re-reads the canonical view.
197
+ 3. Replaces the draft items with the persisted items.
198
+ 4. Sets the parent to the new handle.
199
+ 5. Clears dirty.
200
+ 6. Shows "Saved annotation <id>".
201
+ 7. Refreshes the saved list.
202
+ 3. On failure (403, 404, 409, 413, 422, 500, or network), the UI shows a
203
+ concise message, keeps the draft, and does not retry. It never falls back
204
+ to a new root annotation.
205
+ 4. After a browser reload the token is re-acquired and the saved list rebuilt.
206
+ No draft is recovered, because none is stored in the browser.
207
+
208
+ ## 14. Revision behavior
209
+
210
+ 1. After a save, or after selecting a saved annotation, further edits save as
211
+ a child. The new artifact has a fresh `annotationId` and
212
+ `supersedesAnnotationId` pointing to the parent. It then becomes the loaded
213
+ parent.
214
+ 2. The parent's manifest bytes are unchanged (browser-verified).
215
+ 3. A stale parent gets 409. The UI shows "Save conflict", keeps the draft,
216
+ sends no second POST, and writes nothing.
217
+
218
+ ## 15. Alias identity behavior
219
+
220
+ The browser test:
221
+
222
+ 1. Aliases `current` to observation A.
223
+ 2. Saves annotation X on A.
224
+ 3. Moves `current` to observation B with the canonical `writeAliasCatalog`.
225
+ 4. Restarts the viewer from `loadProjectViewerState`.
226
+
227
+ Result: B (shown as `current`) lists no saved annotations. A, selected by its
228
+ canonical id, still lists X, and X's persisted source still names A.
229
+
230
+ ## 16. Source-unavailable behavior
231
+
232
+ With a draft mark present, the test moves the source observation directory
233
+ away before Save. The POST returns 404 and the UI shows "…no longer exists".
234
+ The draft and its rendered mark remain, no success message appears, and no
235
+ artifact is written. The directory is restored afterwards.
236
+
237
+ ## 17. Accessibility
238
+
239
+ 1. Toolbar mode buttons are native buttons with "<Mode> mode" labels and
240
+ `aria-pressed`.
241
+ 2. The note input and the selected-note textarea have labels.
242
+ 3. In select mode, marks are `role="button"`, `tabIndex=0`, `aria-pressed`,
243
+ with bounded labels such as "Rectangle annotation" or "Note annotation:
244
+ <up to 60 chars>".
245
+ 4. Enter and Space select a mark, mirroring target selection.
246
+ 5. Save status uses `role="status"`, and failures use `role="alert"`.
247
+ 6. Browser-verified: Tab from the last target reaches the first mark, and
248
+ Enter or Space selects it. Target keyboard selection still works and does
249
+ not clear the annotation selection.
250
+
251
+ ## 18. Browser tests
252
+
253
+ New `tests/browser/runtimeAnnotationAuthoring.test.ts` (15 real-Chromium
254
+ tests). It reuses the Prompt 2 fixtures in `tests/support/annotationAuthoringFixtures.ts`.
255
+
256
+ 1. Availability: the project-aware toolbar has all seven modes and Save. The
257
+ standalone viewer has no drawing or save controls, target inspection still
258
+ works, and no non-GET request is sent.
259
+ 2. All five mark kinds are drawn with real pointer input. The persisted kinds,
260
+ coordinates, `uninterpreted` state, id format, and canonical source are
261
+ checked. A note without text is not created.
262
+ 3. Exact rectangle: a reverse drag from 250,220 to 100,120 persists 100, 120,
263
+ 150, 100. A zero-area drag creates nothing.
264
+ 4. Zoomed drawing: after three zoom-ins, a rectangle keeps its source
265
+ coordinates. Fit restores `0 0 800 600`, and target geometry is unchanged.
266
+ 5. Panned drawing and exclusivity: a pan drag changes the viewBox and creates
267
+ no item. A rectangle drag creates an item, leaves the viewBox unchanged,
268
+ and persists the intended source coordinates.
269
+ 6. DPR: at `deviceScaleFactor: 2` (confirmed `devicePixelRatio === 2`), the
270
+ rectangle and point persist CSS-pixel source values, not doubled ones.
271
+ 7. Explicit target association: a rectangle drawn inside `header` stays
272
+ unassociated and does not select the target, until "Associate target
273
+ header". The persisted association is exact.
274
+ 8. Explicit relationship association: the relationship is taken from the
275
+ server graph, the arrow stays unassociated until chosen, and the persisted
276
+ `relationshipKind`, `subjectTarget`, and `relatedTarget` are exact.
277
+ 9. Draft editing:
278
+ 1. Move keeps the id at +40,+30.
279
+ 2. Note edit keeps the anchor and id.
280
+ 3. Save keeps item ids.
281
+ 4. Delete, then Cancel, restores the persisted items.
282
+ 5. Cancel on a new draft empties it.
283
+ 6. No DELETE request is sent.
284
+ 10. Keyboard selection of marks and targets.
285
+ 11. Save and reload: the same annotation id, item ids, rectangle geometry,
286
+ association, and source handle come back from canonical evidence.
287
+ 12. Revision: a child supersedes the parent, the parent bytes are unchanged,
288
+ item ids are preserved, and the child becomes the loaded parent.
289
+ 13. Stale revision: 409, conflict alert, draft kept, exactly one failed POST,
290
+ and nothing written.
291
+ 14. Alias replacement does not rebind (section 15).
292
+ 15. Source disappearance gives a visible failure with the draft kept
293
+ (section 16).
294
+
295
+ Unit tests: new `tests/unit/viewerAnnotationDraftModel.test.ts` (12 tests) covers:
296
+
297
+ 1. Mode set
298
+ 2. Frame validity
299
+ 3. Drawn-mark normalization and refusal
300
+ 4. New-item shape
301
+ 5. Translation
302
+ 6. The shared transform (fake CTM)
303
+ 7. Exact-handle membership
304
+ 8. Failure messages
305
+ 9. Mark and association labels
306
+
307
+ Stabilization notes (test-only):
308
+
309
+ 1. The stale-revision test waits for the post-save saved-list refresh to
310
+ finish before persisting a child from outside the page. Otherwise the
311
+ test's out-of-band writer raced the viewer's own evidence discovery on
312
+ Windows (see section 21).
313
+ 2. The alias test leaves the page before closing the first viewer server, so
314
+ in-flight requests do not hold its connections open during `close()`.
315
+ 3. After these changes the file passed on repeated runs.
316
+
317
+ ## 19. Regression validation
318
+
319
+ Every command below was run on Windows in the repository root.
320
+
321
+ 1. `npm run typecheck`: PASS
322
+ 2. `npm run lint`: PASS
323
+ 3. `npm test`: PASS (77 files, 1298 tests)
324
+ 4. `npm run build`: PASS
325
+ 5. `npm run test:browser`: PASS (22 files, 202 tests, real Chromium)
326
+ 6. `npm run test:security`: PASS (unit: 13 files, 143 tests. Browser: 3 files,
327
+ 77 tests)
328
+ 7. `npm run check:docs`: PASS
329
+ 8. `npm pack --dry-run`: PASS (344 files, 888.8 kB)
330
+
331
+ `tests/browser/observationSvgWorkspace.test.ts` passes unchanged. It covers
332
+ canonical target geometry, unresolved-target honesty, list and SVG selection,
333
+ inspector, and relationships. `projectWorkflowViewer`, `pwaHardening`,
334
+ comparison, reference, binding, fidelity, and context browser suites all
335
+ pass.
336
+
337
+ ## 20. Security regression
338
+
339
+ 1. No server file changed. Host, Origin, token, body-size, path, symlink,
340
+ method, and PWA no-cache tests all pass in `test:security`.
341
+ 2. The viewer sends the token only in the `x-frontend-observer-authoring-token`
342
+ header of same-origin POSTs and stores it nowhere.
343
+ 3. The service-worker configuration is unchanged.
344
+
345
+ ## 21. Scope audit
346
+
347
+ 1. external-reference annotation UI changed: false
348
+ 2. contract promotion implemented: false
349
+ 3. reference materialization implemented: false
350
+ 4. ContractPrimitive vocabulary changed: false
351
+ 5. annotation artifact schema changed: false
352
+ 6. Prompt 2 security policy weakened: false
353
+ 7. project config schema changed: false
354
+ 8. alias catalog schema changed: false
355
+ 9. package version changed: false
356
+ 10. dependency changed: false
357
+
358
+ No file under `src/` changed. `ReferenceWorkspace` and
359
+ `ReferenceRegionOverlaySvg` are untouched. No intent, category, confirmation,
360
+ or contract-primitive UI exists.
361
+
362
+ Remaining risk (pre-existing, not introduced here, protected files not
363
+ modified):
364
+
365
+ 1. On Windows, the canonical artifact writers' atomic directory rename can
366
+ fail with `EPERM` if viewer evidence discovery is reading inside the
367
+ writer's `.tmp-<id>` directory at that moment. That happens when a
368
+ separate process or tab refreshes `/api/index` exactly during a save.
369
+ 2. Normal single-page use does not trigger it, because the UI refreshes only
370
+ after a 201.
371
+ 3. A future batch that owns the writer or discovery could skip `.tmp-*`
372
+ directories during discovery.
373
+
374
+ ## 22. Changed files
375
+
376
+ ### 22.1 Added
377
+
378
+ 1. `viewer/src/types/visualAnnotation.ts`
379
+ 2. `viewer/src/svg/sourceCoordinates.ts`
380
+ 3. `viewer/src/annotation/annotationGeometry.ts` (pure draft geometry and
381
+ mode model, split out so unit tests can reach it)
382
+ 4. `viewer/src/components/AnnotationLayer.tsx`
383
+ 5. `viewer/src/components/AnnotationToolbar.tsx`
384
+ 6. `viewer/src/components/RuntimeAnnotationPanel.tsx`
385
+ 7. `viewer/src/hooks/useAuthoringSession.ts`
386
+ 8. `viewer/src/hooks/useRuntimeAnnotations.ts`
387
+ 9. `viewer/src/hooks/useRuntimeAnnotationDraft.ts`
388
+ 10. `viewer/src/hooks/useAnnotationPointerInteraction.ts` (frame-agnostic
389
+ exclusive pointer binding, reusable by Prompt 4)
390
+ 11. `tests/browser/runtimeAnnotationAuthoring.test.ts`
391
+ 12. `tests/unit/viewerAnnotationDraftModel.test.ts`
392
+ 13. `docs/reports/v0.9-batch3-runtime-screenshot-annotation-authoring.md`
393
+
394
+ ### 22.2 Modified
395
+
396
+ 1. `viewer/src/components/ObservationWorkspace.tsx`
397
+ 2. `viewer/src/components/TargetOverlaySvg.tsx`
398
+ 3. `viewer/src/hooks/useZoomPan.ts`
399
+ 4. `viewer/src/styles/index.css`
400
+ 5. `tests/support/evidenceFixtures.ts`: `writeRichObservationFixture` gains an
401
+ optional `requestId`, because alias catalogs require 64-hex request ids.
402
+ Existing callers are unaffected.
403
+
404
+ ### 22.3 Generated paths
405
+
406
+ 1. `dist/` was rebuilt. It is ignored.
407
+ 2. Tests create and remove temporary directories under the OS temp directory.
408
+ 3. Validation logs went to the session scratchpad.
409
+
410
+ ## 23. Remaining next step
411
+
412
+ v0.9 Prompt 4 — External-reference annotation and reference-region authoring