@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,438 @@
1
+ # v0.9 Batch 2 Report: Viewer Annotation Authoring Boundary
2
+
3
+ ## 1. VERDICT
4
+
5
+ PASS_V0_9_BATCH2_VIEWER_ANNOTATION_AUTHORING_BOUNDARY
6
+
7
+ ## 2. Repository identity
8
+
9
+ 1. Repository: `C:\Users\daile\Projects\my-frontend-observer`
10
+ 2. Branch: `master`
11
+ 3. Starting HEAD: `3618c7b0d2a9cdec80004be4c9cc5bb823bb42f4`
12
+ 4. Ending HEAD before the final commit: `3618c7b0d2a9cdec80004be4c9cc5bb823bb42f4`
13
+ 5. Package version: `0.8.1` (unchanged in `package.json` and `package-lock.json`)
14
+ 6. Viewer protocol version: `1.1.0` (was `1.0.0`)
15
+
16
+ ## 3. Prompt 1 entry gate
17
+
18
+ 1. Prompt 1 verdict: `PASS_V0_9_BATCH1_VISUAL_ANNOTATION_FOUNDATION`
19
+ 2. Prompt 1 commit: `3618c7b0d2a9cdec80004be4c9cc5bb823bb42f4`
20
+ 3. Annotation artifact kind: `my-frontend-observer/visual-annotation`
21
+ 4. Annotation schema: `1.0.0`
22
+ 5. Preflight found a clean tracked worktree on `master` at the required HEAD.
23
+ 6. `git ls-files -v docs/reports/v0.9-architecture-retrieval.md` printed `S`
24
+ at entry and again before commit. The `skip-worktree` state was preserved.
25
+ The user's local copy of that report was not read into this commit or
26
+ changed. The preserved copies under
27
+ `.my-dev-kit-workflow/adhoc/preserved-local-docs/` were not touched.
28
+ 7. No pull, merge, rebase, reset, stash, or clean was run.
29
+
30
+ ## 4. Annotation evidence discovery
31
+
32
+ 1. `ArtifactFamily` in `src/viewerServer/evidence/classify.ts` gains exactly
33
+ `'visual-annotation'`, mapped to `VisualAnnotationArtifact`.
34
+ 2. Classification for `artifactKind === my-frontend-observer/visual-annotation`:
35
+ 1. A schema version other than `1.0.0` gives `unsupported-version` with
36
+ family `visual-annotation`.
37
+ 2. Otherwise the canonical `readVisualAnnotationArtifact` runs. If it
38
+ succeeds, the record is `supported`. If it fails, the record is
39
+ `invalid-structure` with family `visual-annotation`.
40
+ 3. The classifier copies no annotation validation. The reader stays the only
41
+ authority, including its owned-overlay SHA-256 check.
42
+ 4. `src/viewerServer/evidence/handles.ts` gains the `visual-annotation` handle
43
+ slug. This additional file was required because the slug table is typed as
44
+ a complete record over `ArtifactFamily`.
45
+
46
+ ## 5. Annotation metadata projection
47
+
48
+ For a supported annotation, `buildMetadataRecord` returns:
49
+
50
+ 1. `logicalId`: `annotationId`
51
+ 2. `schemaVersion` and `producerVersion` from the artifact
52
+ 3. `annotationSourceKind`: `source.kind`
53
+ 4. `annotationItemCount`: `items.length`
54
+ 5. `annotationConfirmedItemCount`: the number of items whose interpretation
55
+ state is `confirmed`
56
+ 6. `media`: one `annotation-overlay` summary (a cheap availability check only)
57
+ 7. `relatedIds`: `sourceObservationId` or `sourceReferenceId`, plus
58
+ `supersedesAnnotationId` when present
59
+
60
+ Items are never embedded in metadata. Observation alias metadata is applied
61
+ only to observation records, so an annotation record never gets an alias.
62
+
63
+ ## 6. Annotation media resolution
64
+
65
+ 1. `MediaSummary.role` and `MediaRole` gain exactly `annotation-overlay`.
66
+ 2. `resolveMedia` accepts the role only for the `visual-annotation` family. It
67
+ resolves only `artifact.overlay.path` through `resolveContainedFile`. It
68
+ uses `lstat` and requires a regular file. The MIME type is `image/svg+xml`.
69
+ 3. It rejects a missing file, a directory, a symlink or other non-regular
70
+ entry, an unsafe path, the wrong family, an unknown handle, and an unknown
71
+ role. A browser-supplied filename is never used.
72
+ 4. Defense in depth (additional local decision): the served bytes must equal
73
+ `renderVisualAnnotationOverlaySvg(artifact.source, artifact.items)` exactly.
74
+ Otherwise the overlay is refused. A hand-edited SVG whose digest was copied
75
+ into the manifest therefore cannot be served from the viewer origin.
76
+ 5. Defense in depth (additional local decision): every `image/svg+xml` media
77
+ response also carries
78
+ `content-security-policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`
79
+ and `x-content-type-options: nosniff`. Even direct navigation to the
80
+ overlay cannot run script in the viewer origin, where the authoring token
81
+ lives. Existing media types (PNG, JPEG, WebP) get no new headers.
82
+
83
+ ## 7. Annotation source-view projection
84
+
85
+ 1. `src/viewerServer/evidence/index.ts` adds `findObservationByIdentity(root,
86
+ observationId, requestId)` and `findExternalReferenceByIdentity(root,
87
+ referenceId, referenceRequestId)`. Both use `discoverManifests`,
88
+ `MAX_INDEX_RECORDS`, `classifyManifest`, and `encodeArtifactHandle`. Both
89
+ require the logical id and the request id to match.
90
+ 2. Local decision: when more than one exact match exists, both return
91
+ `undefined` instead of picking one. This follows the existing
92
+ `linkedEvidence.ts` rule of never choosing silently.
93
+ 3. New `src/viewerServer/evidence/annotationView.ts` provides
94
+ `getAnnotationView(root, annotationHandle)`. It loads the annotation through
95
+ `loadArtifactByHandle` and requires the `visual-annotation` family. It then
96
+ resolves the source:
97
+ 1. A runtime source maps to the observation handle with media role
98
+ `screenshot`.
99
+ 2. An imported reference maps to the imported handle with role `image`.
100
+ 3. An approved reference maps to the approved handle with role `source-image`.
101
+ 4. A missing or ambiguous source returns `source.status: 'unavailable'` with a
102
+ reason. The annotation itself stays loadable.
103
+ 5. `GET` and `HEAD` `/api/annotations/:handle/view` answer with no-store JSON:
104
+ 1. 400 for a malformed handle.
105
+ 2. 404 for an unknown handle.
106
+ 3. 409 when the handle is not an annotation or not currently loadable.
107
+ 4. 200 otherwise, including when the source is unavailable.
108
+
109
+ ## 8. Project-aware authoring mode
110
+
111
+ 1. `StartViewerOptions` gains `authoringProjectRoot?: string`. When it is
112
+ absent, the viewer is read-only exactly as before.
113
+ 2. When it is supplied, `startViewer` calls the existing
114
+ `loadProjectViewerState`, which validates the project configuration and
115
+ alias catalog. It then requires the viewer root to be that project's
116
+ `projectEvidenceRoot`. The comparison uses resolved paths, with a
117
+ real-path fallback for drive-letter casing and symlinked parents.
118
+ 3. On any mismatch or invalid project, the viewer does not start. It returns
119
+ the existing `viewer-root-invalid` diagnostic. No new diagnostic code was
120
+ added.
121
+ 4. Authoring is never inferred from the root alone.
122
+ 5. `loadProjectViewerState` now also returns `projectRoot`, resolved once.
123
+ Alias behavior is unchanged.
124
+ 6. `src/projectWorkflow/projectPaths.ts` adds `projectAnnotationsRoot(projectRoot)`
125
+ and `annotationOutputLocation()`. The latter returns
126
+ `.frontend-observer/evidence/annotations`. Observation paths, project
127
+ config schema, and catalog schema are unchanged. No migration was added.
128
+ 7. In `src/cli.ts`, the normal `my-frontend-observer view` now also passes
129
+ `authoringProjectRoot` from `loadProjectViewerState`. `view --root <root>`
130
+ passes nothing new, so it stays read-only. No flag, command, or help text
131
+ was added or changed.
132
+
133
+ ## 9. Authoring session capability
134
+
135
+ 1. New `src/viewerServer/authoringSecurity.ts` defines:
136
+ 1. `VIEWER_AUTHORING_TOKEN_BYTES = 32`
137
+ 2. `VIEWER_AUTHORING_TOKEN_HEADER = 'x-frontend-observer-authoring-token'`
138
+ 3. `MAX_ANNOTATION_AUTHORING_BODY_BYTES = 256 * 1024`
139
+ 2. One token is created per authoring server session with
140
+ `randomBytes(32).toString('hex')`, which gives 64 lowercase hex characters.
141
+ It lives only in the in-memory `ViewerAuthoringSession`
142
+ `{ projectRoot, token, expectedHost, expectedOrigin }`. It is never
143
+ persisted, never put in a cookie, storage, a URL, a cache, or an artifact,
144
+ and never logged.
145
+ 3. `expectedHost` and `expectedOrigin` are set after the listener binds to its
146
+ actual port. They are `127.0.0.1:<port>` and `http://127.0.0.1:<port>`.
147
+ Before that they are empty strings, and an empty expectation matches
148
+ nothing.
149
+ 4. `GET` and `HEAD` `/api/authoring/session` always send `cache-control: no-store`:
150
+ 1. A read-only viewer returns `{ ok: true, enabled: false }`.
151
+ 2. An authoring viewer returns `{ ok: true, enabled: true, token }` and
152
+ nothing else.
153
+ 5. Defense in depth (additional local decision): in authoring mode the session
154
+ route returns 403 unless the `Host` header equals `expectedHost`. This keeps
155
+ the token away from a DNS-rebinding page.
156
+
157
+ ## 10. POST security policy
158
+
159
+ `POST /api/annotations` runs these checks in order:
160
+
161
+ 1. Authoring disabled gives 403.
162
+ 2. `Host` must equal `expectedHost`, else 403.
163
+ 3. `Origin` must equal `expectedOrigin` exactly, else 403. A missing Origin is
164
+ also 403.
165
+ 4. The token header must match exactly, else 403. The comparison uses
166
+ `timingSafeEqual` when lengths match, and the response does not say which
167
+ part differed.
168
+ 5. `Content-Type` must be `application/json`, optionally with a UTF-8 charset,
169
+ compared case-insensitively. Anything else gives 415.
170
+ 6. `Content-Encoding` present and not `identity` gives 415.
171
+ 7. A declared `Content-Length` above 262144 gives 413 before any body is read.
172
+ An undeclared chunked body stops being collected once it passes 262144
173
+ bytes and also gives 413. Rejections send `connection: close`, so the
174
+ server stops receiving further body bytes.
175
+ 8. Malformed JSON gives 400.
176
+ 9. Unknown top-level request fields give 400.
177
+
178
+ Method policy:
179
+
180
+ 1. `POST` is accepted only for `/api/annotations`. `GET` or `HEAD` on that path
181
+ returns 405 with `Allow: POST`.
182
+ 2. `POST` on any other path returns 405 with `Allow: GET, HEAD`. This includes
183
+ the Prompt 5 and Prompt 6 route names, which do not exist.
184
+ 3. `PUT`, `PATCH`, and `DELETE` return 405 everywhere.
185
+ 4. No CORS headers are sent.
186
+
187
+ ## 11. Save request contract
188
+
189
+ `SaveAnnotationRequest` is `{ sourceHandle, parentAnnotationHandle?, items }`,
190
+ defined in the new `src/viewerServer/annotationAuthoring.ts`.
191
+
192
+ 1. Exactly those three top-level keys are allowed.
193
+ 2. `sourceHandle` must be a non-empty string.
194
+ 3. `parentAnnotationHandle`, when present, must be a non-empty string.
195
+ 4. `items` must be an array. Its content is validated by the canonical
196
+ annotation domain inside `persistVisualAnnotation`.
197
+ 5. The browser cannot supply a source path, output location, project root,
198
+ annotation id, request id, overlay path or SHA, `createdAt`, producer, or
199
+ alias.
200
+
201
+ ## 12. Server-owned source construction
202
+
203
+ 1. `sourceHandle` is resolved through `loadArtifactByHandle`.
204
+ 1. Unknown gives 404.
205
+ 2. Not loadable gives 409.
206
+ 3. A family other than observation, imported reference, or approved
207
+ reference gives 409.
208
+ 2. Observation: the source is built exactly as the prompt specifies. The
209
+ screenshot must be `available` or `partial`, otherwise 409. The coordinate
210
+ space is `requestConfig.viewport` in CSS pixels. It never uses screenshot
211
+ device pixels or devicePixelRatio.
212
+ 3. Imported reference: built from `referenceId` and `image.sha256`, `width`,
213
+ `height`. The image owner is the reference itself.
214
+ 4. Approved reference: built from `sourceReference.referenceId` and
215
+ `sourceReference.image`.
216
+ 5. An alias is never used as source identity.
217
+
218
+ ## 13. Server-owned output location
219
+
220
+ 1. The service calls `persistVisualAnnotation` exactly once with
221
+ `outputLocation: annotationOutputLocation()` and
222
+ `cwd: session.projectRoot`. Artifacts land in
223
+ `.frontend-observer/evidence/annotations/<annotationId>/`.
224
+ 2. The server never calls the writer directly. It never builds identity or
225
+ overlays itself.
226
+ 3. A 201 response is exactly
227
+ `{ ok, annotationId, annotationRequestId, handle }`. The handle is
228
+ `encodeArtifactHandle('visual-annotation', 'annotations/<annotationId>')`.
229
+ 4. The response carries no absolute path, manifest or overlay path, project
230
+ root, or token.
231
+
232
+ ## 14. Revision and stale-edit behavior
233
+
234
+ 1. With no parent, `supersedesAnnotationId` is undefined.
235
+ 2. With a parent, the parent is resolved through `loadArtifactByHandle` and
236
+ must be `visual-annotation`.
237
+ 1. Unknown gives 404.
238
+ 2. Not an annotation, or not loadable, gives 409.
239
+ 3. `isSameVisualAnnotationSource` compares every canonical source field for
240
+ each source kind. A mismatch gives 409. It is a pure local helper and is
241
+ not exported from the package.
242
+ 4. Stale check: a bounded scan (`discoverManifests`, `MAX_INDEX_RECORDS`,
243
+ `classifyManifest`) looks for any supported annotation whose
244
+ `supersedesAnnotationId` equals the parent id.
245
+ 1. Any such child gives 409, including when several children exist.
246
+ 2. No child is ever selected by timestamp.
247
+ 5. Local decision: if discovery was truncated, the lineage cannot be proven
248
+ free of children. The save fails closed with 409 instead of silently
249
+ branching.
250
+ 6. Local decision: saves are serialized per authoring session with an
251
+ in-process promise queue. The stale check and the write cannot interleave.
252
+ A test sends three concurrent revisions of one parent and gets exactly one
253
+ 201 and two 409s.
254
+ 7. The parent artifact is never modified.
255
+
256
+ ## 15. HTTP status behavior
257
+
258
+ 1. 201: a save succeeded.
259
+ 2. 400: malformed JSON, invalid request shape, an unknown top-level field, a
260
+ malformed view or media handle, or an invalid Content-Length.
261
+ 3. 403: authoring disabled, or a Host, Origin, or token failure. Also the
262
+ session route on an unexpected Host.
263
+ 4. 404: unknown source handle, unknown parent handle, unknown view or media
264
+ handle, or a rejected media role or file.
265
+ 5. 405: an unsupported method or route combination.
266
+ 6. 409: source not loadable or not annotatable, parent not an annotation,
267
+ parent source mismatch, parent already has a child, lineage unverifiable,
268
+ or a view handle that is not a loadable annotation.
269
+ 7. 413: body over 262144 bytes.
270
+ 8. 415: wrong content type or a compressed body.
271
+ 9. 422: canonical annotation validation failure, such as out-of-frame
272
+ geometry or a cross-domain item.
273
+ 10. 500: persistence failure, returned with a generic message and no stack
274
+ trace or path.
275
+
276
+ ## 16. Tests added/modified
277
+
278
+ ### 16.1 Added
279
+
280
+ 1. `tests/support/annotationAuthoringFixtures.ts` holds shared helpers. They
281
+ build real evidence through the canonical writers, create a real
282
+ initialized project, and send raw `node:http` requests with full Host and
283
+ Origin control.
284
+ 2. `tests/unit/viewerAnnotationEvidence.test.ts` (15 tests) covers:
285
+ 1. Discovery A to E: supported metadata and counts, unsupported schema,
286
+ invalid structure, full artifact load, and no alias projection.
287
+ 2. Media: exact SVG, headers, and HEAD.
288
+ 3. Media rejections: wrong family, unknown role, malformed handle, unknown
289
+ handle, traversal handle, missing overlay, directory overlay, symlink
290
+ overlay, and a hand-edited overlay whose digest was updated.
291
+ 4. Source view A to E: runtime, imported, approved, missing source, and
292
+ identity mismatch.
293
+ 5. View status mapping and write-method rejection.
294
+ 3. `tests/unit/viewerAuthoringSecurity.test.ts` (14 tests) covers:
295
+ 1. The constants and protocol version.
296
+ 2. Token format and freshness, and content-type parsing.
297
+ 3. The session route when disabled, enabled, and on a rebinding Host.
298
+ 4. Security cases A to Q, including token lifetime across two servers.
299
+ 5. The exact 262144-byte body and 413 for declared and chunked oversize.
300
+ 6. The method matrix, and read-only routes still working in authoring mode.
301
+ 4. `tests/unit/viewerAnnotationAuthoring.test.ts` (15 tests) covers:
302
+ 1. Project-aware cases A to D.
303
+ 2. Save cases A to C, plus the 404, 409, and 422 mappings.
304
+ 3. Revision cases A to D, plus concurrent revisions.
305
+ 4. Request-shape and same-source helper unit tests.
306
+
307
+ ### 16.2 Modified
308
+
309
+ 1. `tests/unit/projectWorkflow.test.ts` gains 2 tests for the annotation path
310
+ helpers and for `loadProjectViewerState` returning `projectRoot`.
311
+ 2. `tests/unit/cliViewDispatch.test.ts` gains 1 test. Project-aware `view`
312
+ passes `authoringProjectRoot`, and `view --root` does not.
313
+ 3. `tests/browser/projectWorkflowViewer.test.ts` gains 1 real-Chromium test.
314
+ A same-origin page gets the token and saves an annotation (201) with the
315
+ Origin Chromium really sends. The same page gets 403 without the token, and
316
+ 403 on a read-only viewer.
317
+ 4. `tests/browser/pwaHardening.test.ts`: the existing cache-boundary test now
318
+ fetches `/api/authoring/session`, `/api/annotations/:handle/view`,
319
+ `/api/media/:handle/annotation-overlay`, and `POST /api/annotations` from a
320
+ real page. It asserts `no-store` and still finds zero `/api/` cache entries.
321
+ 5. `package.json` (additional file): the `test:security` script now also runs
322
+ the three new unit suites. No version, dependency, or other script changed.
323
+
324
+ No existing test was weakened, skipped, or removed.
325
+
326
+ ## 17. Regression validation
327
+
328
+ Every command below was run on Windows in the repository root.
329
+
330
+ 1. `npm run typecheck`: PASS (exit 0)
331
+ 2. `npm run lint`: PASS (exit 0)
332
+ 3. `npm test`: PASS (76 files, 1286 tests)
333
+ 4. `npm run build`: PASS (exit 0)
334
+ 5. `npm run test:browser`: PASS (21 files, 187 tests, real Chromium)
335
+ 6. `npm run test:security`: PASS (unit: 13 files, 143 tests. Browser: 3 files,
336
+ 77 tests)
337
+ 7. `npm run check:docs`: PASS ("Documentation check passed (17 required files).")
338
+ 8. `npm pack --dry-run`: PASS (343 files, 875.9 kB). It includes the new
339
+ `dist/viewerServer` modules. No agent-control files are included.
340
+
341
+ The existing read-only coverage all still passes: status, index, artifacts,
342
+ observation media, comparison, evaluation, reference, binding, fidelity,
343
+ context, static assets, and GET/HEAD behavior. The packed viewer smoke
344
+ script's check (`POST /api/status` returns 405) still holds by construction.
345
+
346
+ ## 18. Security validation
347
+
348
+ 1. Host enforcement: PASS (wrong and rebinding Host give 403 on save and on
349
+ the session route).
350
+ 2. Origin enforcement: PASS (missing, foreign, `localhost`, and `null` Origin
351
+ give 403. A real Chromium same-origin POST succeeds).
352
+ 3. Token enforcement: PASS (missing, wrong, short, and cross-session tokens
353
+ give 403).
354
+ 4. JSON media type and encoding: PASS (415).
355
+ 5. Body size: PASS (exactly 262144 bytes is accepted. Larger bodies give 413,
356
+ both declared and chunked).
357
+ 6. Path safety: PASS (a traversal handle gives 404, and no browser-chosen
358
+ path or identity is accepted).
359
+ 7. Symlink safety: PASS. File symlink creation was confirmed to work on this
360
+ Windows host, so the symlinked-overlay test really exercised the rejection
361
+ (404). On hosts that refuse to create file symlinks, that test returns
362
+ early by design, the same as the existing viewer symlink test. Directory
363
+ and missing overlay rejection ran unconditionally.
364
+ 8. Method surface: PASS (only `POST /api/annotations`. PUT, PATCH, and DELETE
365
+ are 405 everywhere).
366
+ 9. PWA: PASS (new API responses are `no-store` and never in cache storage).
367
+
368
+ ## 19. Scope audit
369
+
370
+ 1. viewer React UI changed: false
371
+ 2. annotation drawing UI implemented: false
372
+ 3. ContractPrimitive vocabulary changed: false
373
+ 4. contract evaluator changed: false
374
+ 5. external-reference schema changed: false
375
+ 6. reference promotion implemented: false
376
+ 7. contract promotion implemented: false
377
+ 8. project config schema changed: false
378
+ 9. alias catalog schema changed: false
379
+ 10. package version changed: false
380
+ 11. dependency changed: false
381
+
382
+ Prompt 1 owners (`visualAnnotation.ts`, `visualAnnotationIdentity.ts`, the
383
+ annotation reader and writer, and `visualAnnotationPersistenceService.ts`) were
384
+ not modified. No broad documentation file was modified.
385
+
386
+ ## 20. Changed files
387
+
388
+ ### 20.1 Added
389
+
390
+ 1. `src/viewerServer/authoringSecurity.ts`
391
+ 2. `src/viewerServer/annotationAuthoring.ts`
392
+ 3. `src/viewerServer/evidence/annotationView.ts`
393
+ 4. `tests/support/annotationAuthoringFixtures.ts`
394
+ 5. `tests/unit/viewerAnnotationEvidence.test.ts`
395
+ 6. `tests/unit/viewerAuthoringSecurity.test.ts`
396
+ 7. `tests/unit/viewerAnnotationAuthoring.test.ts`
397
+ 8. `docs/reports/v0.9-batch2-viewer-annotation-authoring-boundary.md`
398
+
399
+ ### 20.2 Modified
400
+
401
+ 1. `src/viewerServer/evidence/classify.ts`
402
+ 2. `src/viewerServer/evidence/handles.ts` (additional, see section 4)
403
+ 3. `src/viewerServer/evidence/projection.ts`
404
+ 4. `src/viewerServer/evidence/index.ts`
405
+ 5. `src/viewerServer/evidence/mediaResolver.ts`
406
+ 6. `src/viewerServer/httpServer.ts`
407
+ 7. `src/viewerServer/viewerService.ts`
408
+ 8. `src/projectWorkflow/projectPaths.ts`
409
+ 9. `src/application/projectWorkflowService.ts`
410
+ 10. `src/cli.ts`
411
+ 11. `package.json` (additional, `test:security` script only, see section 16)
412
+ 12. `tests/unit/projectWorkflow.test.ts`
413
+ 13. `tests/unit/cliViewDispatch.test.ts`
414
+ 14. `tests/browser/projectWorkflowViewer.test.ts`
415
+ 15. `tests/browser/pwaHardening.test.ts`
416
+
417
+ ### 20.3 Generated paths
418
+
419
+ 1. `dist/` was rebuilt. It is ignored by Git.
420
+ 2. Tests create temporary directories under the OS temp directory, following
421
+ the existing convention, and remove them.
422
+ 3. The existing PWA test profile under `.my-dev-kit-workflow/v0.8/batch-08/`
423
+ was reused. It is ignored by Git.
424
+ 4. Validation logs were written to the session scratchpad, outside the
425
+ repository.
426
+
427
+ ### 20.4 Remaining risks
428
+
429
+ 1. `startViewer` now imports the project workflow service to reuse
430
+ `loadProjectViewerState`. That module graph was already loaded by the CLI
431
+ and the package entry point.
432
+ 2. `docs/CI_CD.md`, `docs/SECURITY.md`, and `docs/ARCHITECTURE.md` do not yet
433
+ describe the authoring boundary, protocol 1.1.0, or the extended security
434
+ script. Batch 7 owns that reconciliation.
435
+
436
+ ## 21. Remaining next step
437
+
438
+ v0.9 Prompt 3 — Runtime screenshot annotation authoring