artifacty 0.11.0 → 0.12.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.
@@ -1,18 +1,23 @@
1
1
  # Artifacty Enhancement Roadmap Design
2
2
 
3
3
  This document specifies the design for the next set of Artifacty capabilities.
4
- Each feature is written so it can be implemented and tested independently, but
5
- the features are grouped by theme and ordered by recommended priority. Every
6
- feature follows the existing architecture rules: shared behavior lives in
7
- `src/lib/*`, the HTTP, CLI, and MCP surfaces stay thin adapters, versions stay
8
- append-only, artifact content stays untrusted, and no new runtime dependency is
9
- added unless the section says so explicitly.
4
+ Each feature can be implemented and tested independently. Features are grouped
5
+ by theme and listed in the recommended priority order.
10
6
 
11
- Implementation status: all sections below are implemented as of 2026-09-04; `STORE_VERSION` is now `8`. The original planning text follows.
7
+ Every feature follows these architecture rules:
12
8
 
13
- Store schema changes bump `STORE_VERSION` (originally `4`) and use the existing
14
- `ensureColumn` / `CREATE TABLE IF NOT EXISTS` migration path so older stores
15
- upgrade on first access.
9
+ - Put shared behavior in `src/lib/*`.
10
+ - Keep the HTTP, CLI, and MCP interfaces as thin adapters.
11
+ - Keep versions append-only.
12
+ - Treat artifact content as untrusted.
13
+ - Add no runtime dependency unless the section explicitly permits it.
14
+
15
+ Status recorded on 2026-09-04: all sections below were implemented, and
16
+ `STORE_VERSION` was `8`. The original planning text follows.
17
+
18
+ Store schema changes increase `STORE_VERSION` (originally `4`). They use the
19
+ existing `ensureColumn` / `CREATE TABLE IF NOT EXISTS` migration path. Older
20
+ stores upgrade on first access.
16
21
 
17
22
  ## Contents
18
23
 
@@ -78,15 +83,15 @@ MCP tools map the same `code` into `isError: true` results with the code in
78
83
 
79
84
  ### 1.4 Feature flags
80
85
 
81
- New behavior that changes defaults for existing installs is gated by an
82
- environment variable listed in each section, and every flag is reported by
83
- `artifacty doctor` and `artifacty_info`.
86
+ If new behavior changes an existing installation's defaults, control it with an
87
+ environment variable. Each section lists its variable. `artifacty doctor` and
88
+ `artifacty_info` report every flag.
84
89
 
85
90
  ### 1.5 Testing
86
91
 
87
- Each feature adds tests to the matching suite (`storage.test.js`,
88
- `server.test.js`, `mcp-server.test.js`, `cli.test.js`, `converters.test.js`) and,
89
- where behavior is externally visible, a smoke assertion in `npm run smoke`.
92
+ Add tests for each feature to the matching suite: `storage.test.js`,
93
+ `server.test.js`, `mcp-server.test.js`, `cli.test.js`, or `converters.test.js`.
94
+ If the behavior is externally visible, add a smoke assertion in `npm run smoke`.
90
95
 
91
96
  ---
92
97
 
@@ -94,9 +99,8 @@ where behavior is externally visible, a smoke assertion in `npm run smoke`.
94
99
 
95
100
  ### Problem
96
101
 
97
- Artifacts are linked only through shared tags. A handoff, its review, and the
98
- resulting release notes cannot be discovered from one another without knowing
99
- the tag scheme used by the publishing agent.
102
+ Artifacts are linked only through shared tags. To find a handoff, its review,
103
+ and its release notes, users must know the publishing agent's tag scheme.
100
104
 
101
105
  ### Goals
102
106
 
@@ -148,10 +152,10 @@ listRelations(store, id, { direction: "out" | "in" | "both", relation })
148
152
  ```
149
153
 
150
154
  `createArtifact` and `updateArtifact` accept an optional `relations` array
151
- (`[{ toId, relation }]`) so an agent can link in one call. `getArtifact`
152
- returns `relations: { outgoing: [...], incoming: [...] }` with each entry
153
- carrying `toArtifactSummary` of the other side plus `missing: true` when the
154
- target row no longer exists.
155
+ (`[{ toId, relation }]`). This lets an agent add links in the same call.
156
+ `getArtifact` returns `relations: { outgoing: [...], incoming: [...] }`. Each
157
+ entry includes `toArtifactSummary` for the related artifact. If the target row
158
+ no longer exists, the entry includes `missing: true`.
155
159
 
156
160
  ### Surfaces
157
161
 
@@ -165,8 +169,8 @@ target row no longer exists.
165
169
  ### Handoff prompt integration
166
170
 
167
171
  The `artifacty_handoff` and `artifacty_review` prompt templates instruct the
168
- agent to pass `relations: [{ toId: <source>, relation: "derived-from" }]` so the
169
- graph is populated without user intervention.
172
+ agent to pass `relations: [{ toId: <source>, relation: "derived-from" }]`. The
173
+ agent can then add the graph links without user intervention.
170
174
 
171
175
  ### Tests
172
176
 
@@ -217,10 +221,10 @@ Event types: `artifact.created`, `artifact.updated`, `artifact.archived`,
217
221
  `artifact.restored`, `artifact.relation.added`, `artifact.comment.added`,
218
222
  `artifact.version.repaired`, `artifact.version.deleted`.
219
223
 
220
- Events are derived from audit rows. `insertAuditRecord` becomes the single
221
- publish point: after the transaction commits, the storage layer calls
222
- `events.publish(eventFromAudit(row))`. Events are never published for
223
- uncommitted transactions.
224
+ The storage layer derives events from audit rows. `insertAuditRecord` becomes
225
+ the single place that starts event publication. After the transaction commits,
226
+ the storage layer calls `events.publish(eventFromAudit(row))`. It must not
227
+ publish events for uncommitted transactions.
224
228
 
225
229
  ### Persistence
226
230
 
@@ -245,11 +249,13 @@ prune on insert.
245
249
 
246
250
  `GET /api/events` with `Accept: text/event-stream`.
247
251
 
248
- Query filters: `type`, `tag`, `artifactId`, `sourceAgent`. Auth uses the same
249
- token rules as other API routes. Heartbeat comment every 25 seconds. On
250
- connect with `Last-Event-ID`, replay from that sequence. Max concurrent
251
- connections per server: `ARTIFACTY_SSE_MAX_CLIENTS` (default 64); excess
252
- connections get `503`.
252
+ Query filters are `type`, `tag`, `artifactId`, and `sourceAgent`. Authentication
253
+ uses the same token rules as other API routes. The server sends a heartbeat
254
+ comment every 25 seconds. If a client connects with `Last-Event-ID`, the server
255
+ replays events from that sequence.
256
+
257
+ `ARTIFACTY_SSE_MAX_CLIENTS` limits concurrent connections per server (default
258
+ 64). The server returns `503` for connections above this limit.
253
259
 
254
260
  ### Webhooks
255
261
 
@@ -272,8 +278,9 @@ CREATE TABLE IF NOT EXISTS webhooks (
272
278
  - Delivery: `POST` JSON body, headers `X-Artifacty-Event`,
273
279
  `X-Artifacty-Delivery`, `X-Artifacty-Signature: sha256=<hmac>` over the raw
274
280
  body using the secret shown once at creation.
275
- - Retry: 3 attempts with 2s, 10s, 60s backoff. After 20 consecutive failures the
276
- webhook is disabled and a `webhook-deliver-failed` audit row is written.
281
+ - Retry: 3 attempts with delays of 2s, 10s, and 60s. After 20 consecutive
282
+ failures, the server disables the webhook. It writes a
283
+ `webhook-deliver-failed` audit row.
277
284
  - SSRF guard: the target URL must be `http` or `https`; loopback, link-local,
278
285
  and private ranges are rejected unless `ARTIFACTY_WEBHOOK_ALLOW_PRIVATE=true`.
279
286
  Redirects are not followed.
@@ -290,10 +297,10 @@ Routes: `GET/POST /api/webhooks`, `DELETE /api/webhooks/:id`,
290
297
  `notifications/resources/updated` with the URI.
291
298
  - The stdio bridge forwards notifications received from the remote `/mcp`
292
299
  stream to the local client.
293
- - New tool `artifacty_wait` with `{ artifactId?, tag?, type?, timeoutMs }`
294
- blocks up to `timeoutMs` (max 120000) and returns the first matching event or
295
- `{ timedOut: true }`. This gives clients without subscription support a
296
- long-poll primitive.
300
+ - New tool `artifacty_wait` accepts `{ artifactId?, tag?, type?, timeoutMs }`.
301
+ It waits up to `timeoutMs` (max 120000). It returns the first matching event
302
+ or `{ timedOut: true }`. Clients without subscription support can use this
303
+ tool for long polling.
297
304
 
298
305
  ### CLI
299
306
 
@@ -311,17 +318,17 @@ MCP subscribe round trip through the HTTP transport, `artifacty_wait` timeout.
311
318
 
312
319
  ### Problem
313
320
 
314
- Two agents that both read version 3 and both call `artifacty_update` produce
315
- versions 4 and 5. The second write silently discards the first agent's work
316
- from the "latest" view.
321
+ If two agents read version 3 and both call `artifacty_update`, they produce
322
+ versions 4 and 5. The latest version then omits the first agent's work without
323
+ reporting a conflict.
317
324
 
318
325
  ### Design
319
326
 
320
327
  - Every artifact response exposes `latestVersion` (already present) and an
321
328
  `etag` string equal to `"<id>:<latestVersion>"`.
322
- - `updateArtifact` accepts `expectedVersion` (number). Inside the existing
323
- transaction, if `artifact.latestVersion !== expectedVersion`, throw
324
- `VersionConflictError` carrying `latestVersion`, and write an
329
+ - `updateArtifact` accepts `expectedVersion` (number). In the existing
330
+ transaction, check whether `artifact.latestVersion !== expectedVersion`.
331
+ If the versions differ, throw `VersionConflictError` with `latestVersion`. Write an
325
332
  `update-conflict` audit row.
326
333
  - HTTP: `POST /api/artifacts/:id/versions` (and the browser edit form) accept
327
334
  `If-Match: "<etag>"` or body `expectedVersion`. Conflict returns `409` with
@@ -332,9 +339,9 @@ from the "latest" view.
332
339
  `artifacty_get` output already carries `latestVersion`; the tool description
333
340
  tells agents to pass it back.
334
341
  - CLI: `artifacty update --expected-version N`.
335
- - Browser editor: hidden field with the version being edited; on `409` the
336
- editor shows a banner with a link to the diff between the edited base and the
337
- new latest version, and keeps the unsaved text.
342
+ - Browser editor: use a hidden field for the version being edited. On `409`,
343
+ keep the unsaved text and show a banner. The banner links to a diff between
344
+ the edited base version and the new latest version.
338
345
 
339
346
  `expectedVersion` is optional so existing clients keep working. A future
340
347
  `ARTIFACTY_REQUIRE_EXPECTED_VERSION=true` flag can make it mandatory for API
@@ -352,8 +359,8 @@ with matching version, `If-Match` header parsing including weak validators,
352
359
 
353
360
  ### Problem
354
361
 
355
- Feedback on an artifact today requires publishing a whole new version or a
356
- separate review artifact. Lightweight, version-anchored notes are missing.
362
+ Feedback on an artifact requires a new version or a separate review artifact.
363
+ Users cannot add a short note to a specific version.
357
364
 
358
365
  ### Data model
359
366
 
@@ -382,18 +389,18 @@ CREATE INDEX IF NOT EXISTS idx_comments_artifact ON artifact_comments(artifact_i
382
389
  formats, `{ "path": "$.runs[0].results[3]" }` for JSON/SARIF, `{ "row": 7 }`
383
390
  for CSV. Anchors are hints for rendering and are not validated against
384
391
  content.
385
- - `body` is Markdown, rendered through the same sanitized Markdown pipeline as
386
- artifact content and size-capped at 16 KB.
392
+ - `body` is Markdown with a 16 KB size limit. The renderer uses the same
393
+ sanitized Markdown pipeline as artifact content.
387
394
  - Comments are soft-deleted; the audit log keeps the action.
388
395
  - Threads are one level deep (`parent_id` points to a root comment).
389
396
 
390
397
  ### Review state
391
398
 
392
- An artifact-level `reviewStatus` column (`none`, `pending`, `changes-requested`,
393
- `approved`) is added to `artifacts`. It is set explicitly via
394
- `POST /api/artifacts/:id/review-status` and reset to `pending` automatically
395
- when a new version is appended after an approval, which is recorded in the
396
- audit metadata. This is deliberately minimal: no multi-approver rules.
399
+ Add a `reviewStatus` column to `artifacts`. Its values are `none`, `pending`,
400
+ `changes-requested`, and `approved`. Set it explicitly through
401
+ `POST /api/artifacts/:id/review-status`. If a new version is appended after an
402
+ approval, reset the status to `pending` and record the reset in audit metadata.
403
+ This design does not include rules for multiple approvers.
397
404
 
398
405
  ### Surfaces
399
406
 
@@ -420,18 +427,19 @@ sanitization of comment bodies.
420
427
 
421
428
  ### Problem
422
429
 
423
- FTS5 handles keyword queries. Natural-language questions such as "the analysis
424
- of last week's failed deploy" miss when the wording differs.
430
+ FTS5 handles keyword queries. A natural-language question can miss relevant
431
+ results if it uses different words. For example: "the analysis of last week's
432
+ failed deploy".
425
433
 
426
434
  ### Design principles
427
435
 
428
- - No mandatory dependency and no bundled model. Embeddings come from a
429
- pluggable provider.
430
- - Semantic search is additive: when disabled or unavailable, behavior is
431
- unchanged.
432
- - Vectors are stored in SQLite as BLOBs; similarity is computed in JavaScript.
433
- This is adequate for the expected store size (tens of thousands of artifacts)
434
- and avoids a native vector extension.
436
+ - No mandatory dependency and no bundled model. A configurable provider
437
+ supplies embeddings.
438
+ - If semantic search is disabled or unavailable, keep the existing search
439
+ behavior.
440
+ - Store vectors in SQLite as BLOBs. Compute similarity in JavaScript. This
441
+ supports the expected store size of tens of thousands of artifacts without
442
+ a native vector extension.
435
443
 
436
444
  ### Provider interface (`src/lib/embeddings.js`)
437
445
 
@@ -472,17 +480,18 @@ The embedded text is `title + tags + metadata summary + first
472
480
  ARTIFACTY_EMBEDDINGS_MAX_CHARS (default 8000) characters of the latest version`.
473
481
  Binary formats (`image`, `video`) embed metadata only.
474
482
 
475
- Indexing runs through the existing background module (`src/lib/background.js`)
476
- after each create/update so writes never wait on a network call. Failures are
477
- logged and retried by `artifacty index rebuild --embeddings`.
483
+ Run indexing through the existing background module (`src/lib/background.js`)
484
+ after each create or update. Writes must not wait for a network call. Log
485
+ failures. Use `artifacty index rebuild --embeddings` to retry indexing.
478
486
 
479
487
  ### Query
480
488
 
481
489
  `GET /api/artifacts?q=...&mode=semantic|keyword|hybrid`.
482
490
 
483
491
  - `keyword`: current FTS5 path.
484
- - `semantic`: embed the query, cosine similarity over all vectors for the
485
- configured provider/model, top `limit` after `offset`.
492
+ - `semantic`: embed the query. Calculate cosine similarity across all vectors
493
+ for the configured provider and model. Return the top `limit` results after
494
+ `offset`.
486
495
  - `hybrid` (default when a provider is configured): reciprocal rank fusion of
487
496
  keyword and semantic rankings, `k = 60`.
488
497
 
@@ -504,11 +513,11 @@ Completes the "Future Extensions" list in `docs/sarif-csv-artifact-plan.md`.
504
513
 
505
514
  ### Design
506
515
 
507
- - Rendering stays server-side and bounded; interactivity is progressive
508
- enhancement in `src/client/viewer.js` using data already in the table.
509
- - SARIF viewer: level filter chips (`error`, `warning`, `note`), rule id text
510
- filter, sort by level, rule, or location. Filtering operates on the bounded
511
- set already rendered; a notice states when results were truncated.
516
+ - The server renders a limited set of results. `src/client/viewer.js` adds
517
+ browser controls that use the data already in the table.
518
+ - SARIF viewer: add level filter chips (`error`, `warning`, `note`) and a rule
519
+ id text filter. Allow sorting by level, rule, or location. Filter only the
520
+ rendered results. Show a notice if the result set was truncated.
512
521
  - CSV viewer: click column header to sort; per-column contains filter; row
513
522
  count shown.
514
523
  - Download: `GET /artifacts/:id/export?format=csv&filter=...&sort=...` for CSV,
@@ -529,10 +538,10 @@ byte cap, viewer script has no inline event handlers (CSP compatibility).
529
538
 
530
539
  ### Design
531
540
 
532
- - List filters extend to `artifactType`, `publisher`, `createdAfter`,
533
- `createdBefore`, `reviewStatus`, `relatedTo`, and `mode` (search). All are
534
- query-string driven so URLs remain shareable and the CLI/MCP list surfaces
535
- reuse the same `listArtifactsPage` filters.
541
+ - Add list filters for `artifactType`, `publisher`, `createdAfter`,
542
+ `createdBefore`, `reviewStatus`, `relatedTo`, and `mode` (search). Put the
543
+ filters in query strings so users can share URLs. The CLI and MCP list
544
+ interfaces reuse the same `listArtifactsPage` filters.
536
545
  - Saved views:
537
546
 
538
547
  ```sql
@@ -547,8 +556,9 @@ CREATE TABLE IF NOT EXISTS saved_views (
547
556
  );
548
557
  ```
549
558
 
550
- In single-user mode (no users table rows) views are global; in team mode they
551
- belong to a user and can be marked `shared` so they appear for everyone.
559
+ In single-user mode (no users table rows), views are global. In team mode,
560
+ each view belongs to a user. Mark a view as `shared` to make it appear for
561
+ everyone.
552
562
 
553
563
  - Routes: `GET/POST /api/views`, `DELETE /api/views/:id`; browser sidebar
554
564
  lists views and a "Save current filters" action.
@@ -572,12 +582,12 @@ Artifacts, audit rows, and (after section 3) events grow without bound.
572
582
 
573
583
  ### Design
574
584
 
575
- Policies are declarative and evaluated by a background sweep in
576
- `src/lib/background.js` every `ARTIFACTY_RETENTION_INTERVAL` (default 1h) and
577
- on demand via `artifacty retention run`.
585
+ Define retention rules in a policy. A background sweep in
586
+ `src/lib/background.js` evaluates the policy every `ARTIFACTY_RETENTION_INTERVAL`
587
+ (default 1h). Run `artifacty retention run` to evaluate it on demand.
578
588
 
579
- Configuration lives in the `meta` table as JSON under key `retention_policy`
580
- and is edited through `/admin/retention` or `artifacty retention set`:
589
+ Store the configuration as JSON in the `meta` table under `retention_policy`.
590
+ Edit it through `/admin/retention` or `artifacty retention set`:
581
591
 
582
592
  ```json
583
593
  {
@@ -603,8 +613,8 @@ Semantics:
603
613
  - `auditRetentionDays`: audit rows older than the window are deleted, except
604
614
  `version-repair`, `version-delete`, `retention-purge`, and
605
615
  `owner-change` which are kept indefinitely.
606
- - Every sweep writes a summary row (`retention-sweep`) so operators can see
607
- when it ran and what it touched.
616
+ - Every sweep writes a summary row (`retention-sweep`). The row shows when
617
+ the sweep ran and what it changed.
608
618
 
609
619
  `artifacty integrity` gains a check that no orphaned files remain after a
610
620
  purge.
@@ -631,9 +641,9 @@ New columns on `artifacts`:
631
641
  - `visibility TEXT NOT NULL DEFAULT 'team'` with values `private`, `team`.
632
642
  - `owner_user_id TEXT` backfilled from `publisher_user_id` on migration.
633
643
 
634
- Optional teams are out of scope for v1; `team` means "all authenticated users
635
- of this server". A later version can add a `groups` table without changing
636
- the API shape.
644
+ Optional teams are outside the v1 scope. In this design, `team` means "all
645
+ authenticated users of this server". A later version can add a `groups` table
646
+ without changing the API shape.
637
647
 
638
648
  ### Rules
639
649
 
@@ -657,8 +667,9 @@ the API shape.
657
667
 
658
668
  ### Browser
659
669
 
660
- Viewer shows a visibility badge; the edit form offers the toggle to owners and
661
- admins; the account page lists "My private artifacts".
670
+ The viewer shows a visibility badge. The edit form gives owners and
671
+ administrators a visibility toggle. The account page lists "My private
672
+ artifacts".
662
673
 
663
674
  ### Tests
664
675
 
@@ -682,11 +693,11 @@ Scopes:
682
693
  | `write` | create, import, update, link, comment, archive/restore of own artifacts |
683
694
  | `admin` | everything the user role allows; only available to admin users |
684
695
 
685
- - `authenticateApiToken` returns `scopes`; a new `requireScope(auth, scope)`
686
- helper in `src/lib/security.js` is called by each HTTP route and MCP tool
687
- handler. Denials return `403` with `code: "scope_denied"` and write a
688
- `token-scope-denied` audit row (rate-limited to one per token per minute to
689
- avoid log flooding).
696
+ - `authenticateApiToken` returns `scopes`. Each HTTP route and MCP tool handler
697
+ calls a new `requireScope(auth, scope)` helper in `src/lib/security.js`.
698
+ A denial returns `403` with `code: "scope_denied"` and writes a
699
+ `token-scope-denied` audit row. Limit these audit rows to one per token per
700
+ minute to prevent excessive logging.
690
701
  - MCP `tools/list` filters out mutating tools when the authenticated token
691
702
  lacks `write`, so read-only agents never see tools they cannot call.
692
703
  - `/account` token creation form adds scope checkboxes; `artifacty token`
@@ -704,8 +715,8 @@ admin role, default for legacy tokens.
704
715
 
705
716
  ### Design
706
717
 
707
- A fixed-window counter in memory keyed by `(principal, bucket)` where principal
708
- is the token id, user id, or remote address, in that order of preference.
718
+ Use a fixed-window counter in memory, keyed by `(principal, bucket)`. Select
719
+ the principal in this order: token id, user id, then remote address.
709
720
 
710
721
  | Bucket | Default limit | Env |
711
722
  | --- | --- | --- |
@@ -731,8 +742,8 @@ header.
731
742
 
732
743
  ### Problem
733
744
 
734
- The current bundle excludes users, sessions, tokens, and audit logs, so a
735
- server move needs manual steps.
745
+ The current bundle excludes users, sessions, tokens, and audit logs. Moving
746
+ the store to another server therefore requires manual steps.
736
747
 
737
748
  ### Design
738
749
 
@@ -762,11 +773,11 @@ sessions absent, file mode.
762
773
 
763
774
  ### Design
764
775
 
765
- - Fenced code blocks in Markdown artifacts get syntax highlighting using the
766
- already-vendored CodeMirror language packages in read-only mode, applied
767
- client-side by `viewer.js` to `<pre><code class="language-x">` elements.
768
- Server output remains plain escaped HTML so no-JS and CSP-strict contexts
769
- still work.
776
+ - Use the bundled CodeMirror language packages in read-only mode to highlight
777
+ fenced code blocks in Markdown artifacts. In the browser, `viewer.js` applies
778
+ highlighting to `<pre><code class="language-x">` elements. The server returns
779
+ plain escaped HTML. Rendering must still work without JavaScript and with
780
+ strict CSP settings.
770
781
  - ```` ```mermaid ```` fences render through the existing sandboxed Mermaid
771
782
  iframe path, one iframe per diagram, lazily created when scrolled into view.
772
783
  A per-document cap (`ARTIFACTY_MAX_INLINE_DIAGRAMS`, default 20) prevents
@@ -789,14 +800,14 @@ sanitization unchanged for inline HTML in Markdown.
789
800
  - New format `notebook` (`.ipynb`, `application/x-ipynb+json`), default
790
801
  artifact type `analysis-report`.
791
802
  - Import detection: JSON object with `nbformat` and `cells[]`.
792
- - Converter produces a normalized structure for rendering only; the stored
793
- content is the original notebook JSON.
794
- - Viewer renders cells in order: Markdown cells through the Markdown pipeline,
795
- code cells as highlighted source, outputs limited to `text/plain`,
796
- `text/markdown`, `image/png`, `image/jpeg`, `image/svg+xml` (sanitized, in
797
- the scriptless SVG iframe). `text/html` outputs render inside the sandboxed
798
- HTML iframe like HTML artifacts. Other MIME types show a placeholder with
799
- the type name.
803
+ - The converter produces a normalized structure only for rendering. Store
804
+ the original notebook JSON.
805
+ - The viewer renders cells in order. Render Markdown cells through the
806
+ Markdown pipeline and code cells as highlighted source. Allow these output
807
+ types: `text/plain`, `text/markdown`, `image/png`, `image/jpeg`, and
808
+ `image/svg+xml`. Sanitize SVG output and render it in the scriptless SVG
809
+ iframe. Render `text/html` output in the sandboxed HTML iframe, as for HTML
810
+ artifacts. For other MIME types, show a placeholder with the type name.
800
811
  - Size guard: outputs larger than 2 MB are replaced with a "truncated" notice.
801
812
  - `artifacty import --agent generic --file notebook.ipynb` and the browser
802
813
  import page detect it automatically.
@@ -820,8 +831,8 @@ createStructuredDiff(before, after, { format })
820
831
 
821
832
  | Format | Strategy |
822
833
  | --- | --- |
823
- | `json`, `sarif`, `notebook` | Recursive object diff producing `added`, `removed`, `changed` entries keyed by JSON path; arrays of objects with an `id`, `ruleId`, or `path` key are matched by that key, otherwise by index. |
824
- | `csv` | Header-aware row diff: rows matched by the first column when it is unique, otherwise by position; reports added, removed, changed cells. |
834
+ | `json`, `sarif`, `notebook` | Compare objects recursively. Produce `added`, `removed`, and `changed` entries keyed by JSON path. Match array objects by `id`, `ruleId`, or `path` when available. Otherwise, match by index. |
835
+ | `csv` | Compare rows using their headers. If the first column is unique, match rows by that column. Otherwise, match by position. Report added, removed, and changed cells. |
825
836
  | `markdown`, `text`, `code`, `html` | Existing line diff plus word-level highlighting inside changed lines. |
826
837
  | `bundle` | Per-file diff using the strategy of each file's format. |
827
838
 
@@ -873,10 +884,10 @@ artifacty watch --artifact release-handoff-abc12345 --exec "./on-change.sh"
873
884
  ```
874
885
 
875
886
  - Connects to `/api/events` using the URL from `server.json` or
876
- `ARTIFACTY_URL`, reconnects with `Last-Event-ID` on drop.
887
+ `ARTIFACTY_URL`. If the connection drops, reconnects with `Last-Event-ID`.
877
888
  - Prints one JSON line per event with `--json`, or a human line otherwise.
878
- - `--exec` runs the command with the event JSON on stdin and `ARTIFACTY_EVENT_*`
879
- environment variables; failures are printed but do not stop the watch.
889
+ - `--exec` runs the command with event JSON on stdin and `ARTIFACTY_EVENT_*`
890
+ environment variables. It prints failures and continues watching.
880
891
  - `--once` exits after the first match (a shell-friendly `artifacty_wait`).
881
892
 
882
893
  ### `artifacty diff`
@@ -904,8 +915,9 @@ code, `--exec` environment injection.
904
915
  ### Design
905
916
 
906
917
  - `src/lib/openapi.js` builds an OpenAPI 3.1 document from a single route
907
- table that `server.js` also uses for dispatch, so the spec cannot drift
908
- from the implementation. The route table entry shape:
918
+ table. `server.js` uses the same table to dispatch requests. This keeps the
919
+ specification consistent with the implementation. Each route entry has this
920
+ shape:
909
921
 
910
922
  ```js
911
923
  { method: "POST", path: "/api/artifacts", handler, auth: "write", summary, requestSchema, responseSchema }
@@ -926,16 +938,16 @@ code, `--exec` environment injection.
926
938
 
927
939
  ### Design
928
940
 
929
- - Re-verify the hand-rolled implementation against the newest MCP
930
- specification before implementing sections 3 and 5. The checklist:
941
+ - Before implementing sections 3 and 5, check the custom implementation
942
+ against the newest MCP specification. Use this checklist:
931
943
  - Negotiate the newest protocol version while continuing to accept
932
944
  `2025-06-18` from older clients.
933
945
  - `tools/list` `outputSchema` for every tool that returns
934
946
  `structuredContent`, generated from the same schema table as section 19.
935
947
  - `resources/subscribe`, `listChanged` notifications (section 3).
936
- - Elicitation: if the client advertises `elicitation`, `artifacty_update`
937
- without `expectedVersion` on a conflict may ask the user whether to rebase;
938
- otherwise return the conflict error.
948
+ - Elicitation: if a conflict occurs and the client advertises `elicitation`,
949
+ `artifacty_update` without `expectedVersion` may ask the user whether to
950
+ rebase. Otherwise, return the conflict error.
939
951
  - Pagination cursors on `tools/list`, `resources/list`, and
940
952
  `prompts/list` when result counts exceed 50.
941
953
  - Streamable HTTP session resumption on `/mcp` (`Mcp-Session-Id`, event
@@ -961,10 +973,10 @@ subscribe flow, cursor pagination, session resume through the HTTP transport.
961
973
  | 4 | 5 Comments, 8 Filters and views, 9 Retention | 8 | Day-to-day usability and long-running store health. |
962
974
  | 5 | 6 Semantic search, 14 Markdown rendering, 15 Notebook, 7 SARIF/CSV, 17 Document assets | 8 (no bump; new tables are created idempotently) | Content and discovery improvements that build on everything above. |
963
975
 
964
- Each phase ends with `npm run release:check`, an update to `README.md`,
965
- `docs/mcp-public-api.md`, `docs/threat-model.md`, and `docs/artifact-schema-v1.md`
966
- (or a `v2` schema document when the artifact envelope changes), and a
967
- `STORE_VERSION` bump only when a table or column was added in that phase.
976
+ At the end of each phase, run `npm run release:check`. Update `README.md`,
977
+ `docs/mcp-public-api.md`, `docs/threat-model.md`, and `docs/artifact-schema-v1.md`.
978
+ If the artifact envelope changes, use a `v2` schema document. Increase
979
+ `STORE_VERSION` only if the phase added a table or column.
968
980
 
969
981
  ### Compatibility guarantees across all phases
970
982
 
@@ -1,38 +1,37 @@
1
1
  # SARIF and CSV Artifact Support
2
2
 
3
- Artifacty supports SARIF and CSV as **output artifacts** that Codex and other
4
- agents can hand off for downstream review. This scope excludes Codex input
5
- context, appshots, thread state, and client-specific UI state.
3
+ Artifacty supports SARIF and CSV as **output artifacts**. Codex and other agents
4
+ can share these artifacts for review. This scope excludes Codex input context,
5
+ appshots, thread state, and client-specific UI state.
6
6
 
7
7
  ## Implemented Behavior
8
8
 
9
- - `sarif` and `csv` are first-class `format` values across HTTP, CLI, MCP,
10
- storage, and browser forms.
9
+ - HTTP, CLI, MCP, storage, and browser forms support `sarif` and `csv` as
10
+ `format` values.
11
11
  - `.sarif`, `.sarif.json`, `application/sarif+json`, `.csv`, and `text/csv`
12
- inputs are detected during import.
13
- - SARIF top-level objects with `version` and `runs[]` are imported as
12
+ inputs are detected by the importer.
13
+ - The importer stores SARIF top-level objects with `version` and `runs[]` as
14
14
  `analysis-report` artifacts.
15
- - CSV inputs default to `table`; CSV files that look like security or review
16
- findings infer `analysis-report`.
15
+ - CSV inputs default to `table`. If a CSV file appears to contain security
16
+ or review findings, the importer selects `analysis-report`.
17
17
  - `/raw` always returns the original stored source, unaffected by rendering,
18
18
  sort/filter, or export.
19
19
 
20
20
  ## Browser Rendering
21
21
 
22
- - SARIF renders a bounded findings summary with run, result, error, warning,
23
- and note counts.
22
+ - The SARIF viewer limits the findings summary size. The summary includes run,
23
+ result, error, warning, and note counts.
24
24
  - SARIF result rows show level, rule id, message, first location, and tool name.
25
25
  - The full formatted SARIF JSON remains available in a details panel.
26
26
  - CSV renders as an escaped table with bounded rows and columns.
27
- - Malformed CSV or non-SARIF JSON fails closed to escaped source or formatted
28
- JSON fallback.
27
+ - If CSV is malformed or JSON is not SARIF, the viewer displays escaped source
28
+ or formatted JSON. It does not render an unsafe table.
29
29
 
30
30
  ## Sort, Filter, and Download (roadmap section 7)
31
31
 
32
- - The rendered SARIF and CSV tables are progressively enhanced client-side in
33
- `src/client/viewer.js`, over the data already in the bounded server-rendered
34
- set; the server output is fully correct and readable with JavaScript
35
- disabled.
32
+ - The server renders a limited set of SARIF results or CSV rows.
33
+ `src/client/viewer.js` adds controls to that data in the browser. The server
34
+ output remains correct and readable when JavaScript is disabled.
36
35
  - SARIF: level filter chips (`error`, `warning`, `note`), a rule id text
37
36
  filter, and click-to-sort on the Level/Rule/Location columns, plus a
38
37
  visible result count.
@@ -47,17 +46,17 @@ context, appshots, thread state, and client-specific UI state.
47
46
  comma-separated subset of `error`, `warning`, `note`, `none`; `rule` is a
48
47
  case-insensitive rule id substring match. The exported document keeps
49
48
  only the matching results per run and stays structurally valid SARIF.
50
- - The route re-parses the stored original content (never `/raw` itself),
51
- streams the result with `Content-Disposition: attachment` and the
52
- matching content type, and is capped at the store's max artifact byte
53
- size (trailing rows/results are dropped deterministically once the cap
54
- would be exceeded).
49
+ - The route parses the original stored content again. It does not read
50
+ `/raw`. It streams the result with `Content-Disposition: attachment` and
51
+ the matching content type. The export cannot exceed the store's maximum
52
+ artifact byte size. The route removes trailing rows or results in a fixed
53
+ order when they would exceed that limit.
55
54
  - Invalid parameters (unknown format, unknown sort/filter column, bad
56
55
  `dir`, unknown SARIF level, or an artifact whose stored format doesn't
57
56
  match `format`) return HTTP 400 with `code: "invalid_export"`.
58
- - Shared filter/sort/serialization logic lives in
59
- `src/lib/sarif-csv-export.js`, independent of the presentation-focused
60
- parsing in `src/lib/render.js`.
57
+ - Shared filter, sort, and serialization functions are in
58
+ `src/lib/sarif-csv-export.js`. They are separate from the display parsing
59
+ functions in `src/lib/render.js`.
61
60
 
62
61
  ## Verification Coverage
63
62
 
@@ -76,4 +75,4 @@ context, appshots, thread state, and client-specific UI state.
76
75
 
77
76
  ## Future Extensions
78
77
 
79
- - None currently planned; see roadmap-design.md for the broader roadmap.
78
+ - No extensions are currently planned. See roadmap-design.md for the full roadmap.