artifacty 0.10.8 → 0.11.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.
- package/CLAUDE.md +1 -1
- package/README.md +260 -5
- package/docs/artifact-schema-v1.md +187 -1
- package/docs/central-team-deployment-design.md +3 -1
- package/docs/integrations.md +9 -1
- package/docs/mcp-public-api.md +103 -9
- package/docs/release-checklist.md +1 -1
- package/docs/roadmap-design.md +977 -0
- package/docs/sarif-csv-artifact-plan.md +42 -7
- package/docs/threat-model.md +217 -2
- package/package.json +2 -1
- package/src/cli.js +550 -43
- package/src/client/viewer.js +377 -0
- package/src/lib/backup.js +326 -15
- package/src/lib/converters.js +99 -5
- package/src/lib/csv.js +51 -0
- package/src/lib/diff.js +656 -0
- package/src/lib/doctor.js +88 -2
- package/src/lib/embeddings.js +407 -0
- package/src/lib/events.js +183 -0
- package/src/lib/i18n.js +290 -2
- package/src/lib/listing.js +46 -0
- package/src/lib/openapi.js +614 -0
- package/src/lib/render.js +1604 -160
- package/src/lib/retention-form.js +31 -0
- package/src/lib/retention.js +499 -0
- package/src/lib/sarif-csv-export.js +290 -0
- package/src/lib/schemas.js +461 -0
- package/src/lib/security.js +226 -1
- package/src/lib/sse.js +52 -0
- package/src/lib/storage.js +2935 -305
- package/src/lib/webhooks.js +356 -0
- package/src/mcp-server.js +756 -25
- package/src/server.js +1529 -98
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
# Artifacty Enhancement Roadmap Design
|
|
2
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
Implementation status: all sections below are implemented as of 2026-09-04; `STORE_VERSION` is now `8`. The original planning text follows.
|
|
12
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
1. [Cross-Cutting Conventions](#1-cross-cutting-conventions)
|
|
20
|
+
2. [Artifact Relations](#2-artifact-relations)
|
|
21
|
+
3. [Change Notifications](#3-change-notifications)
|
|
22
|
+
4. [Optimistic Concurrency](#4-optimistic-concurrency)
|
|
23
|
+
5. [Comments and Review Threads](#5-comments-and-review-threads)
|
|
24
|
+
6. [Semantic Search](#6-semantic-search)
|
|
25
|
+
7. [SARIF and CSV Sort, Filter, Download](#7-sarif-and-csv-sort-filter-download)
|
|
26
|
+
8. [Dashboard Filters and Saved Views](#8-dashboard-filters-and-saved-views)
|
|
27
|
+
9. [Retention Policies](#9-retention-policies)
|
|
28
|
+
10. [Artifact Visibility and Ownership](#10-artifact-visibility-and-ownership)
|
|
29
|
+
11. [API Token Scopes](#11-api-token-scopes)
|
|
30
|
+
12. [Rate Limiting](#12-rate-limiting)
|
|
31
|
+
13. [Full Backup Bundles](#13-full-backup-bundles)
|
|
32
|
+
14. [Markdown Embedded Rendering](#14-markdown-embedded-rendering)
|
|
33
|
+
15. [Jupyter Notebook Format](#15-jupyter-notebook-format)
|
|
34
|
+
16. [Structured Diff](#16-structured-diff)
|
|
35
|
+
17. [Document Assets in Bundles](#17-document-assets-in-bundles)
|
|
36
|
+
18. [CLI Watch and Diff Commands](#18-cli-watch-and-diff-commands)
|
|
37
|
+
19. [OpenAPI Specification](#19-openapi-specification)
|
|
38
|
+
20. [MCP Protocol Refresh](#20-mcp-protocol-refresh)
|
|
39
|
+
21. [Delivery Plan](#21-delivery-plan)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 1. Cross-Cutting Conventions
|
|
44
|
+
|
|
45
|
+
### 1.1 Module placement
|
|
46
|
+
|
|
47
|
+
| Concern | Module |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Relations, comments, retention, visibility, scopes | `src/lib/storage.js` (or split into `src/lib/storage/*.js` if the file passes ~3000 lines) |
|
|
50
|
+
| Event bus and SSE/webhook fan-out | `src/lib/events.js` (new) |
|
|
51
|
+
| Embeddings | `src/lib/embeddings.js` (new) |
|
|
52
|
+
| Structured diff | `src/lib/diff.js` (extend) |
|
|
53
|
+
| Rate limiting | `src/lib/security.js` (extend) |
|
|
54
|
+
| OpenAPI document | `src/lib/openapi.js` (new) |
|
|
55
|
+
| Notebook conversion | `src/lib/converters.js` (extend) |
|
|
56
|
+
|
|
57
|
+
### 1.2 Audit actions
|
|
58
|
+
|
|
59
|
+
Every new mutation writes an audit row through `insertAuditRecord`. New action
|
|
60
|
+
names introduced in this document:
|
|
61
|
+
|
|
62
|
+
`relation-add`, `relation-remove`, `comment-add`, `comment-resolve`,
|
|
63
|
+
`comment-delete`, `webhook-create`, `webhook-delete`, `webhook-deliver-failed`,
|
|
64
|
+
`retention-archive`, `retention-purge`, `visibility-change`, `owner-change`,
|
|
65
|
+
`token-scope-denied`, `update-conflict`, `rate-limited`.
|
|
66
|
+
|
|
67
|
+
### 1.3 Error shape
|
|
68
|
+
|
|
69
|
+
HTTP JSON errors keep the existing `{ error: string }` body and add an optional
|
|
70
|
+
machine-readable `code`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{ "error": "Version conflict", "code": "version_conflict", "details": { "latestVersion": 4 } }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
MCP tools map the same `code` into `isError: true` results with the code in
|
|
77
|
+
`structuredContent.code`.
|
|
78
|
+
|
|
79
|
+
### 1.4 Feature flags
|
|
80
|
+
|
|
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`.
|
|
84
|
+
|
|
85
|
+
### 1.5 Testing
|
|
86
|
+
|
|
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`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 2. Artifact Relations
|
|
94
|
+
|
|
95
|
+
### Problem
|
|
96
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
### Goals
|
|
102
|
+
|
|
103
|
+
- Typed, directional links between artifacts.
|
|
104
|
+
- Discoverable from list, get, MCP resources, and the browser viewer.
|
|
105
|
+
- Links survive archive and restore; a dangling link is reported, not deleted.
|
|
106
|
+
|
|
107
|
+
### Data model
|
|
108
|
+
|
|
109
|
+
New table:
|
|
110
|
+
|
|
111
|
+
```sql
|
|
112
|
+
CREATE TABLE IF NOT EXISTS artifact_relations (
|
|
113
|
+
id TEXT PRIMARY KEY,
|
|
114
|
+
from_id TEXT NOT NULL,
|
|
115
|
+
to_id TEXT NOT NULL,
|
|
116
|
+
relation TEXT NOT NULL,
|
|
117
|
+
created_at TEXT NOT NULL,
|
|
118
|
+
created_by TEXT,
|
|
119
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
120
|
+
UNIQUE (from_id, to_id, relation),
|
|
121
|
+
FOREIGN KEY (from_id) REFERENCES artifacts(id) ON DELETE CASCADE,
|
|
122
|
+
FOREIGN KEY (to_id) REFERENCES artifacts(id) ON DELETE CASCADE
|
|
123
|
+
);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON artifact_relations(from_id);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON artifact_relations(to_id);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Relation vocabulary (validated, closed set for v1):
|
|
129
|
+
|
|
130
|
+
| Relation | Meaning |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `derived-from` | `from` was produced by reading `to` |
|
|
133
|
+
| `supersedes` | `from` replaces `to` |
|
|
134
|
+
| `reviews` | `from` is a review of `to` |
|
|
135
|
+
| `references` | loose citation |
|
|
136
|
+
| `part-of` | `from` belongs to bundle/collection `to` |
|
|
137
|
+
|
|
138
|
+
Inverse names are computed, not stored (`derived-from` ↔ `derives`,
|
|
139
|
+
`supersedes` ↔ `superseded-by`, `reviews` ↔ `reviewed-by`, `part-of` ↔
|
|
140
|
+
`contains`, `references` ↔ `referenced-by`).
|
|
141
|
+
|
|
142
|
+
### Storage API
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
addRelation(store, { fromId, toId, relation, audit, metadata })
|
|
146
|
+
removeRelation(store, { fromId, toId, relation, audit })
|
|
147
|
+
listRelations(store, id, { direction: "out" | "in" | "both", relation })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`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
|
+
|
|
156
|
+
### Surfaces
|
|
157
|
+
|
|
158
|
+
| Surface | Change |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| HTTP | `GET /api/artifacts/:id/relations`, `POST /api/artifacts/:id/relations` (body `{ toId, relation }`), `DELETE /api/artifacts/:id/relations/:relationId`. `GET /api/artifacts` accepts `relatedTo=<id>` and `relation=<name>`. |
|
|
161
|
+
| MCP | `artifacty_link`, `artifacty_unlink` tools; `artifacty_get` output includes `relations`; `artifacty_list` accepts `relatedTo`. New resource `artifacty://artifacts/{id}/graph` returning a depth-2 adjacency list. |
|
|
162
|
+
| CLI | `artifacty link <from> <relation> <to>`, `artifacty unlink ...`, `artifacty show --relations`. |
|
|
163
|
+
| Browser | Viewer sidebar "Related" panel; edit form gets a relation picker with search. |
|
|
164
|
+
|
|
165
|
+
### Handoff prompt integration
|
|
166
|
+
|
|
167
|
+
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.
|
|
170
|
+
|
|
171
|
+
### Tests
|
|
172
|
+
|
|
173
|
+
Round trip, uniqueness constraint, cascade on artifact delete, dangling
|
|
174
|
+
reporting after admin version delete does not touch relations, `relatedTo`
|
|
175
|
+
filter with pagination, MCP tool schema exposure.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 3. Change Notifications
|
|
180
|
+
|
|
181
|
+
### Problem
|
|
182
|
+
|
|
183
|
+
Agents that wait for another agent's output must poll `artifacty_list`.
|
|
184
|
+
There is no push channel.
|
|
185
|
+
|
|
186
|
+
### Goals
|
|
187
|
+
|
|
188
|
+
- In-process event bus that every mutation publishes to.
|
|
189
|
+
- Browser and CLI consumers via Server-Sent Events.
|
|
190
|
+
- External consumers via signed webhooks.
|
|
191
|
+
- MCP consumers via `notifications/resources/updated` when the transport
|
|
192
|
+
supports it (`/mcp` HTTP transport and the stdio bridge).
|
|
193
|
+
|
|
194
|
+
### Non-goals
|
|
195
|
+
|
|
196
|
+
- Durable delivery guarantees beyond bounded retry.
|
|
197
|
+
- Cross-process fan-out between two server processes sharing one store.
|
|
198
|
+
|
|
199
|
+
### Event model
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
{
|
|
203
|
+
"id": "evt_01J...",
|
|
204
|
+
"type": "artifact.updated",
|
|
205
|
+
"createdAt": "2026-09-04T10:00:00.000Z",
|
|
206
|
+
"artifactId": "release-handoff-abc12345",
|
|
207
|
+
"version": 3,
|
|
208
|
+
"actor": "user@example.com",
|
|
209
|
+
"sourceAgent": "claude",
|
|
210
|
+
"surface": "mcp",
|
|
211
|
+
"tags": ["handoff"],
|
|
212
|
+
"artifactType": "handoff"
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Event types: `artifact.created`, `artifact.updated`, `artifact.archived`,
|
|
217
|
+
`artifact.restored`, `artifact.relation.added`, `artifact.comment.added`,
|
|
218
|
+
`artifact.version.repaired`, `artifact.version.deleted`.
|
|
219
|
+
|
|
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
|
+
|
|
225
|
+
### Persistence
|
|
226
|
+
|
|
227
|
+
Events are stored for replay in a bounded table so a reconnecting SSE client
|
|
228
|
+
can resume with `Last-Event-ID`:
|
|
229
|
+
|
|
230
|
+
```sql
|
|
231
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
232
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
233
|
+
id TEXT NOT NULL UNIQUE,
|
|
234
|
+
created_at TEXT NOT NULL,
|
|
235
|
+
type TEXT NOT NULL,
|
|
236
|
+
artifact_id TEXT,
|
|
237
|
+
payload_json TEXT NOT NULL
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Retention: keep the newest `ARTIFACTY_EVENT_HISTORY` rows (default 10000);
|
|
242
|
+
prune on insert.
|
|
243
|
+
|
|
244
|
+
### SSE endpoint
|
|
245
|
+
|
|
246
|
+
`GET /api/events` with `Accept: text/event-stream`.
|
|
247
|
+
|
|
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`.
|
|
253
|
+
|
|
254
|
+
### Webhooks
|
|
255
|
+
|
|
256
|
+
```sql
|
|
257
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
258
|
+
id TEXT PRIMARY KEY,
|
|
259
|
+
url TEXT NOT NULL,
|
|
260
|
+
secret_hash TEXT NOT NULL,
|
|
261
|
+
event_types_json TEXT NOT NULL,
|
|
262
|
+
filter_json TEXT NOT NULL DEFAULT '{}',
|
|
263
|
+
owner_user_id TEXT,
|
|
264
|
+
created_at TEXT NOT NULL,
|
|
265
|
+
disabled_at TEXT,
|
|
266
|
+
last_delivery_at TEXT,
|
|
267
|
+
last_status INTEGER,
|
|
268
|
+
failure_count INTEGER NOT NULL DEFAULT 0
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- Delivery: `POST` JSON body, headers `X-Artifacty-Event`,
|
|
273
|
+
`X-Artifacty-Delivery`, `X-Artifacty-Signature: sha256=<hmac>` over the raw
|
|
274
|
+
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.
|
|
277
|
+
- SSRF guard: the target URL must be `http` or `https`; loopback, link-local,
|
|
278
|
+
and private ranges are rejected unless `ARTIFACTY_WEBHOOK_ALLOW_PRIVATE=true`.
|
|
279
|
+
Redirects are not followed.
|
|
280
|
+
- Admin only for creation in team mode; the token owner in single-user mode.
|
|
281
|
+
|
|
282
|
+
Routes: `GET/POST /api/webhooks`, `DELETE /api/webhooks/:id`,
|
|
283
|
+
`POST /api/webhooks/:id/test`. Browser page `/admin/webhooks`.
|
|
284
|
+
|
|
285
|
+
### MCP
|
|
286
|
+
|
|
287
|
+
- `initialize` advertises `resources: { subscribe: true, listChanged: true }`.
|
|
288
|
+
- `resources/subscribe` on `artifacty://artifacts/{id}` or `artifacty://recent`
|
|
289
|
+
registers the session; matching events send
|
|
290
|
+
`notifications/resources/updated` with the URI.
|
|
291
|
+
- The stdio bridge forwards notifications received from the remote `/mcp`
|
|
292
|
+
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.
|
|
297
|
+
|
|
298
|
+
### CLI
|
|
299
|
+
|
|
300
|
+
See section 18 for `artifacty watch`.
|
|
301
|
+
|
|
302
|
+
### Tests
|
|
303
|
+
|
|
304
|
+
Event ordering matches audit sequence, replay by `Last-Event-ID`, filter
|
|
305
|
+
matching, webhook signature verification, SSRF rejection, retry then disable,
|
|
306
|
+
MCP subscribe round trip through the HTTP transport, `artifacty_wait` timeout.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 4. Optimistic Concurrency
|
|
311
|
+
|
|
312
|
+
### Problem
|
|
313
|
+
|
|
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.
|
|
317
|
+
|
|
318
|
+
### Design
|
|
319
|
+
|
|
320
|
+
- Every artifact response exposes `latestVersion` (already present) and an
|
|
321
|
+
`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
|
|
325
|
+
`update-conflict` audit row.
|
|
326
|
+
- HTTP: `POST /api/artifacts/:id/versions` (and the browser edit form) accept
|
|
327
|
+
`If-Match: "<etag>"` or body `expectedVersion`. Conflict returns `409` with
|
|
328
|
+
`code: "version_conflict"` and the current summary so the client can rebase.
|
|
329
|
+
`GET /api/artifacts/:id` returns `ETag` and honors `If-None-Match` with
|
|
330
|
+
`304`.
|
|
331
|
+
- MCP: `artifacty_update` gains optional `expectedVersion`. The
|
|
332
|
+
`artifacty_get` output already carries `latestVersion`; the tool description
|
|
333
|
+
tells agents to pass it back.
|
|
334
|
+
- 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.
|
|
338
|
+
|
|
339
|
+
`expectedVersion` is optional so existing clients keep working. A future
|
|
340
|
+
`ARTIFACTY_REQUIRE_EXPECTED_VERSION=true` flag can make it mandatory for API
|
|
341
|
+
and MCP writes.
|
|
342
|
+
|
|
343
|
+
### Tests
|
|
344
|
+
|
|
345
|
+
Conflict raised inside the transaction (no version file written), success path
|
|
346
|
+
with matching version, `If-Match` header parsing including weak validators,
|
|
347
|
+
`304` on `If-None-Match`, browser banner rendering, MCP conflict result shape.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 5. Comments and Review Threads
|
|
352
|
+
|
|
353
|
+
### Problem
|
|
354
|
+
|
|
355
|
+
Feedback on an artifact today requires publishing a whole new version or a
|
|
356
|
+
separate review artifact. Lightweight, version-anchored notes are missing.
|
|
357
|
+
|
|
358
|
+
### Data model
|
|
359
|
+
|
|
360
|
+
```sql
|
|
361
|
+
CREATE TABLE IF NOT EXISTS artifact_comments (
|
|
362
|
+
id TEXT PRIMARY KEY,
|
|
363
|
+
artifact_id TEXT NOT NULL,
|
|
364
|
+
version INTEGER NOT NULL,
|
|
365
|
+
parent_id TEXT,
|
|
366
|
+
author_user_id TEXT,
|
|
367
|
+
author_label TEXT NOT NULL,
|
|
368
|
+
source_agent TEXT,
|
|
369
|
+
body TEXT NOT NULL,
|
|
370
|
+
anchor_json TEXT,
|
|
371
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
372
|
+
created_at TEXT NOT NULL,
|
|
373
|
+
resolved_at TEXT,
|
|
374
|
+
resolved_by TEXT,
|
|
375
|
+
deleted_at TEXT,
|
|
376
|
+
FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE CASCADE
|
|
377
|
+
);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS idx_comments_artifact ON artifact_comments(artifact_id, version);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- `anchor_json` is optional and format-specific: `{ "line": 42 }` for text
|
|
382
|
+
formats, `{ "path": "$.runs[0].results[3]" }` for JSON/SARIF, `{ "row": 7 }`
|
|
383
|
+
for CSV. Anchors are hints for rendering and are not validated against
|
|
384
|
+
content.
|
|
385
|
+
- `body` is Markdown, rendered through the same sanitized Markdown pipeline as
|
|
386
|
+
artifact content and size-capped at 16 KB.
|
|
387
|
+
- Comments are soft-deleted; the audit log keeps the action.
|
|
388
|
+
- Threads are one level deep (`parent_id` points to a root comment).
|
|
389
|
+
|
|
390
|
+
### Review state
|
|
391
|
+
|
|
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.
|
|
397
|
+
|
|
398
|
+
### Surfaces
|
|
399
|
+
|
|
400
|
+
| Surface | Change |
|
|
401
|
+
| --- | --- |
|
|
402
|
+
| HTTP | `GET/POST /api/artifacts/:id/comments`, `POST /api/artifacts/:id/comments/:cid/resolve`, `DELETE /api/artifacts/:id/comments/:cid`, `POST /api/artifacts/:id/review-status`. |
|
|
403
|
+
| MCP | `artifacty_comment` `{ id, version?, body, anchor?, parentId? }`, `artifacty_resolve_comment`, `artifacty_set_review_status`. `artifacty_get` gains `includeComments` (default false, returns open comments on the requested version). |
|
|
404
|
+
| CLI | `artifacty comment <id> --body ...`, `artifacty comments <id>`. |
|
|
405
|
+
| Browser | Viewer side panel listing comments by version with resolve/reply; line-anchored comments show gutter markers in the CodeMirror read-only viewer. |
|
|
406
|
+
| Events | `artifact.comment.added`, `artifact.review_status.changed`. |
|
|
407
|
+
|
|
408
|
+
The `artifacty_review` prompt template is updated to prefer comments over
|
|
409
|
+
publishing a separate review artifact when the review is short.
|
|
410
|
+
|
|
411
|
+
### Tests
|
|
412
|
+
|
|
413
|
+
Thread depth limit, anchor pass-through, soft delete hides from list but audit
|
|
414
|
+
remains, review status reset on new version, MCP tool round trip, Markdown
|
|
415
|
+
sanitization of comment bodies.
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## 6. Semantic Search
|
|
420
|
+
|
|
421
|
+
### Problem
|
|
422
|
+
|
|
423
|
+
FTS5 handles keyword queries. Natural-language questions such as "the analysis
|
|
424
|
+
of last week's failed deploy" miss when the wording differs.
|
|
425
|
+
|
|
426
|
+
### Design principles
|
|
427
|
+
|
|
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.
|
|
435
|
+
|
|
436
|
+
### Provider interface (`src/lib/embeddings.js`)
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
export function createEmbeddingProvider(config) // returns null when disabled
|
|
440
|
+
provider.name // "openai-compatible" | "command" | "none"
|
|
441
|
+
provider.dimensions // integer
|
|
442
|
+
provider.embed(texts) // Promise<Float32Array[]>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Providers in v1:
|
|
446
|
+
|
|
447
|
+
| Provider | Config |
|
|
448
|
+
| --- | --- |
|
|
449
|
+
| `openai-compatible` | `ARTIFACTY_EMBEDDINGS_URL`, `ARTIFACTY_EMBEDDINGS_MODEL`, `ARTIFACTY_EMBEDDINGS_API_KEY` (calls `POST {url}/embeddings`) |
|
|
450
|
+
| `command` | `ARTIFACTY_EMBEDDINGS_COMMAND` (a local executable that reads JSON lines on stdin and writes vectors on stdout, so users can wire Ollama or any local model without Artifacty depending on it) |
|
|
451
|
+
|
|
452
|
+
The API key is read from the environment only and never written to the store
|
|
453
|
+
or logs.
|
|
454
|
+
|
|
455
|
+
### Storage
|
|
456
|
+
|
|
457
|
+
```sql
|
|
458
|
+
CREATE TABLE IF NOT EXISTS artifact_embeddings (
|
|
459
|
+
artifact_id TEXT NOT NULL,
|
|
460
|
+
version INTEGER NOT NULL,
|
|
461
|
+
provider TEXT NOT NULL,
|
|
462
|
+
model TEXT NOT NULL,
|
|
463
|
+
dimensions INTEGER NOT NULL,
|
|
464
|
+
vector BLOB NOT NULL,
|
|
465
|
+
created_at TEXT NOT NULL,
|
|
466
|
+
PRIMARY KEY (artifact_id, provider, model),
|
|
467
|
+
FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE CASCADE
|
|
468
|
+
);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The embedded text is `title + tags + metadata summary + first
|
|
472
|
+
ARTIFACTY_EMBEDDINGS_MAX_CHARS (default 8000) characters of the latest version`.
|
|
473
|
+
Binary formats (`image`, `video`) embed metadata only.
|
|
474
|
+
|
|
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`.
|
|
478
|
+
|
|
479
|
+
### Query
|
|
480
|
+
|
|
481
|
+
`GET /api/artifacts?q=...&mode=semantic|keyword|hybrid`.
|
|
482
|
+
|
|
483
|
+
- `keyword`: current FTS5 path.
|
|
484
|
+
- `semantic`: embed the query, cosine similarity over all vectors for the
|
|
485
|
+
configured provider/model, top `limit` after `offset`.
|
|
486
|
+
- `hybrid` (default when a provider is configured): reciprocal rank fusion of
|
|
487
|
+
keyword and semantic rankings, `k = 60`.
|
|
488
|
+
|
|
489
|
+
Responses add `search.mode` and per-row `search_score`. `artifacty_list` gets
|
|
490
|
+
the same `mode` argument. The dashboard exposes a mode toggle only when
|
|
491
|
+
`artifacty_info` reports a provider.
|
|
492
|
+
|
|
493
|
+
### Tests
|
|
494
|
+
|
|
495
|
+
Provider-less path unchanged, `command` provider with a fixture script,
|
|
496
|
+
cosine ordering, RRF merge determinism, rebuild command, secret redaction of
|
|
497
|
+
the API key in doctor output.
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## 7. SARIF and CSV Sort, Filter, Download
|
|
502
|
+
|
|
503
|
+
Completes the "Future Extensions" list in `docs/sarif-csv-artifact-plan.md`.
|
|
504
|
+
|
|
505
|
+
### Design
|
|
506
|
+
|
|
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.
|
|
512
|
+
- CSV viewer: click column header to sort; per-column contains filter; row
|
|
513
|
+
count shown.
|
|
514
|
+
- Download: `GET /artifacts/:id/export?format=csv&filter=...&sort=...` for CSV,
|
|
515
|
+
and `?format=sarif&level=error` for SARIF. The server re-parses the stored
|
|
516
|
+
original, applies the filter, and streams a fresh file. `/raw` is untouched.
|
|
517
|
+
Filtered exports are capped at `MAX_ARTIFACT_BYTES`.
|
|
518
|
+
- Real-world fixtures for CodeQL, Semgrep, and Trivy are added under
|
|
519
|
+
`test/fixtures/sarif/` and used in converter and server tests.
|
|
520
|
+
|
|
521
|
+
### Tests
|
|
522
|
+
|
|
523
|
+
Fixture parsing, filter/sort parameter validation, export content type and
|
|
524
|
+
byte cap, viewer script has no inline event handlers (CSP compatibility).
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## 8. Dashboard Filters and Saved Views
|
|
529
|
+
|
|
530
|
+
### Design
|
|
531
|
+
|
|
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.
|
|
536
|
+
- Saved views:
|
|
537
|
+
|
|
538
|
+
```sql
|
|
539
|
+
CREATE TABLE IF NOT EXISTS saved_views (
|
|
540
|
+
id TEXT PRIMARY KEY,
|
|
541
|
+
owner_user_id TEXT,
|
|
542
|
+
name TEXT NOT NULL,
|
|
543
|
+
filters_json TEXT NOT NULL,
|
|
544
|
+
shared INTEGER NOT NULL DEFAULT 0,
|
|
545
|
+
created_at TEXT NOT NULL,
|
|
546
|
+
updated_at TEXT NOT NULL
|
|
547
|
+
);
|
|
548
|
+
```
|
|
549
|
+
|
|
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.
|
|
552
|
+
|
|
553
|
+
- Routes: `GET/POST /api/views`, `DELETE /api/views/:id`; browser sidebar
|
|
554
|
+
lists views and a "Save current filters" action.
|
|
555
|
+
- MCP: `artifacty_list` accepts `view: "<name or id>"` which expands to the
|
|
556
|
+
saved filters, so a prompt can say "list the `open-reviews` view".
|
|
557
|
+
- Dashboard grouping: optional `groupBy=artifactType|sourceAgent|day` renders
|
|
558
|
+
section headers; purely presentational.
|
|
559
|
+
|
|
560
|
+
### Tests
|
|
561
|
+
|
|
562
|
+
Filter parsing edge cases (invalid dates), view ownership and sharing
|
|
563
|
+
visibility, MCP view expansion, i18n keys present for `en` and `ko`.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 9. Retention Policies
|
|
568
|
+
|
|
569
|
+
### Problem
|
|
570
|
+
|
|
571
|
+
Artifacts, audit rows, and (after section 3) events grow without bound.
|
|
572
|
+
|
|
573
|
+
### Design
|
|
574
|
+
|
|
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`.
|
|
578
|
+
|
|
579
|
+
Configuration lives in the `meta` table as JSON under key `retention_policy`
|
|
580
|
+
and is edited through `/admin/retention` or `artifacty retention set`:
|
|
581
|
+
|
|
582
|
+
```json
|
|
583
|
+
{
|
|
584
|
+
"archiveAfterDays": { "default": null, "byType": { "test-report": 30 } },
|
|
585
|
+
"purgeArchivedAfterDays": 180,
|
|
586
|
+
"auditRetentionDays": 365,
|
|
587
|
+
"eventRetentionRows": 10000,
|
|
588
|
+
"keepTags": ["pinned", "release"]
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Semantics:
|
|
593
|
+
|
|
594
|
+
- `archiveAfterDays`: artifacts not updated within the window are archived
|
|
595
|
+
(`retention-archive` audit action, actor `system:retention`). Artifacts with
|
|
596
|
+
a tag in `keepTags` or with `reviewStatus = approved` are skipped.
|
|
597
|
+
- `purgeArchivedAfterDays`: archived artifacts older than the window are
|
|
598
|
+
hard-deleted, including version files, relations, comments, and embeddings.
|
|
599
|
+
This is the only path that deletes whole artifacts, and it always runs with
|
|
600
|
+
a dry-run report first (`artifacty retention run --dry-run`) that admins can
|
|
601
|
+
inspect on `/admin/retention`. Purge is disabled unless
|
|
602
|
+
`ARTIFACTY_RETENTION_ALLOW_PURGE=true`.
|
|
603
|
+
- `auditRetentionDays`: audit rows older than the window are deleted, except
|
|
604
|
+
`version-repair`, `version-delete`, `retention-purge`, and
|
|
605
|
+
`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.
|
|
608
|
+
|
|
609
|
+
`artifacty integrity` gains a check that no orphaned files remain after a
|
|
610
|
+
purge.
|
|
611
|
+
|
|
612
|
+
### Tests
|
|
613
|
+
|
|
614
|
+
Policy parsing and defaults, `keepTags` exemption, dry-run produces no
|
|
615
|
+
changes, purge removes files and dependent rows, audit exemptions, sweep
|
|
616
|
+
idempotency.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## 10. Artifact Visibility and Ownership
|
|
621
|
+
|
|
622
|
+
### Problem
|
|
623
|
+
|
|
624
|
+
In team mode every user sees and can update every artifact. There are only two
|
|
625
|
+
roles.
|
|
626
|
+
|
|
627
|
+
### Data model
|
|
628
|
+
|
|
629
|
+
New columns on `artifacts`:
|
|
630
|
+
|
|
631
|
+
- `visibility TEXT NOT NULL DEFAULT 'team'` with values `private`, `team`.
|
|
632
|
+
- `owner_user_id TEXT` backfilled from `publisher_user_id` on migration.
|
|
633
|
+
|
|
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.
|
|
637
|
+
|
|
638
|
+
### Rules
|
|
639
|
+
|
|
640
|
+
| Action | `private` | `team` |
|
|
641
|
+
| --- | --- | --- |
|
|
642
|
+
| Read, list, raw, diff | owner, admin | any authenticated user |
|
|
643
|
+
| Update, comment, link | owner, admin | any authenticated user unless `ARTIFACTY_TEAM_WRITE=owner` |
|
|
644
|
+
| Archive, restore, change visibility, change owner | owner, admin | owner, admin |
|
|
645
|
+
| Admin version repair/delete | admin | admin |
|
|
646
|
+
|
|
647
|
+
- In single-user mode (no users) everything is allowed as today.
|
|
648
|
+
- Anonymous access with a shared `ARTIFACTY_API_TOKEN` but no personal token
|
|
649
|
+
is treated as `team` read/write and cannot see `private` artifacts.
|
|
650
|
+
- List queries add a `WHERE visibility = 'team' OR owner_user_id = ?` clause;
|
|
651
|
+
FTS and semantic search apply the same predicate before ranking.
|
|
652
|
+
- `createArtifact` accepts `visibility`; MCP `artifacty_create`/`update`
|
|
653
|
+
expose it with a description that defaults to `team`.
|
|
654
|
+
- `POST /api/artifacts/:id/visibility` and `POST /api/artifacts/:id/owner`
|
|
655
|
+
(admin or owner) write `visibility-change` / `owner-change` audit rows.
|
|
656
|
+
- Backup export includes both columns; import of an older bundle defaults them.
|
|
657
|
+
|
|
658
|
+
### Browser
|
|
659
|
+
|
|
660
|
+
Viewer shows a visibility badge; the edit form offers the toggle to owners and
|
|
661
|
+
admins; the account page lists "My private artifacts".
|
|
662
|
+
|
|
663
|
+
### Tests
|
|
664
|
+
|
|
665
|
+
Predicate applied on every read path including `/raw`, relation listing hides
|
|
666
|
+
private targets from non-owners (returns `restricted: true` instead of the
|
|
667
|
+
summary), MCP resource read denial, migration backfill of `owner_user_id`.
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## 11. API Token Scopes
|
|
672
|
+
|
|
673
|
+
### Design
|
|
674
|
+
|
|
675
|
+
`api_tokens` gains `scopes_json TEXT NOT NULL DEFAULT '["read","write"]'`.
|
|
676
|
+
|
|
677
|
+
Scopes:
|
|
678
|
+
|
|
679
|
+
| Scope | Grants |
|
|
680
|
+
| --- | --- |
|
|
681
|
+
| `read` | list, get, raw, diff, relations, comments (read), events (SSE) |
|
|
682
|
+
| `write` | create, import, update, link, comment, archive/restore of own artifacts |
|
|
683
|
+
| `admin` | everything the user role allows; only available to admin users |
|
|
684
|
+
|
|
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).
|
|
690
|
+
- MCP `tools/list` filters out mutating tools when the authenticated token
|
|
691
|
+
lacks `write`, so read-only agents never see tools they cannot call.
|
|
692
|
+
- `/account` token creation form adds scope checkboxes; `artifacty token`
|
|
693
|
+
CLI (server-issued personal tokens) gains `--scope read`.
|
|
694
|
+
- Existing tokens keep full scopes through the column default.
|
|
695
|
+
|
|
696
|
+
### Tests
|
|
697
|
+
|
|
698
|
+
Scope enforcement per route, MCP tool list filtering, admin scope requires
|
|
699
|
+
admin role, default for legacy tokens.
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## 12. Rate Limiting
|
|
704
|
+
|
|
705
|
+
### Design
|
|
706
|
+
|
|
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.
|
|
709
|
+
|
|
710
|
+
| Bucket | Default limit | Env |
|
|
711
|
+
| --- | --- | --- |
|
|
712
|
+
| `write` (create, import, update, comment, link) | 120 / minute | `ARTIFACTY_RATE_WRITE_PER_MIN` |
|
|
713
|
+
| `auth` (login, token exchange) | 10 / minute per address | `ARTIFACTY_RATE_AUTH_PER_MIN` |
|
|
714
|
+
| `search` | 300 / minute | `ARTIFACTY_RATE_SEARCH_PER_MIN` |
|
|
715
|
+
|
|
716
|
+
- Disabled on loopback binds unless `ARTIFACTY_RATE_LIMIT=always`.
|
|
717
|
+
- Responses over the limit return `429` with `Retry-After` and
|
|
718
|
+
`code: "rate_limited"`. MCP tools return the same as an error result.
|
|
719
|
+
- Body size limits already exist (`MAX_ARTIFACT_BYTES`, `MAX_BACKUP_BYTES`);
|
|
720
|
+
this section adds `MAX_COMMENT_BYTES` (16 KB) and documents all limits in
|
|
721
|
+
`docs/threat-model.md`.
|
|
722
|
+
|
|
723
|
+
### Tests
|
|
724
|
+
|
|
725
|
+
Window reset, principal selection order, loopback bypass, `Retry-After`
|
|
726
|
+
header.
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## 13. Full Backup Bundles
|
|
731
|
+
|
|
732
|
+
### Problem
|
|
733
|
+
|
|
734
|
+
The current bundle excludes users, sessions, tokens, and audit logs, so a
|
|
735
|
+
server move needs manual steps.
|
|
736
|
+
|
|
737
|
+
### Design
|
|
738
|
+
|
|
739
|
+
- `buildStoreBackup(store, { scope: "artifacts" | "full" })`. `full` adds
|
|
740
|
+
`users` (with password hashes), `api_tokens` (hashes and scopes),
|
|
741
|
+
`audit_log`, `artifact_relations`, `artifact_comments`, `saved_views`,
|
|
742
|
+
`webhooks` (without secrets; they must be re-issued), and `meta` policy
|
|
743
|
+
keys. Sessions are never exported.
|
|
744
|
+
- Bundle header gains `bundleVersion: 2`, `scope`, and `storeVersion`.
|
|
745
|
+
Version 1 bundles import as before.
|
|
746
|
+
- Import of a `full` bundle is admin-only, requires
|
|
747
|
+
`confirm: "replace-all"` in the request body, and refuses when the target
|
|
748
|
+
store already has users unless `--force-users` is passed. It runs in one
|
|
749
|
+
transaction and writes a `backup-import` audit row summarizing counts.
|
|
750
|
+
- CLI: `artifacty backup --full`, `artifacty import-store --file x.json`
|
|
751
|
+
auto-detects scope; `artifacty export` keeps the artifacts-only default.
|
|
752
|
+
- Bundles containing password or token hashes are written with mode `0600`.
|
|
753
|
+
|
|
754
|
+
### Tests
|
|
755
|
+
|
|
756
|
+
Round trip of every table, v1 bundle compatibility, refusal conditions,
|
|
757
|
+
sessions absent, file mode.
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 14. Markdown Embedded Rendering
|
|
762
|
+
|
|
763
|
+
### Design
|
|
764
|
+
|
|
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.
|
|
770
|
+
- ```` ```mermaid ```` fences render through the existing sandboxed Mermaid
|
|
771
|
+
iframe path, one iframe per diagram, lazily created when scrolled into view.
|
|
772
|
+
A per-document cap (`ARTIFACTY_MAX_INLINE_DIAGRAMS`, default 20) prevents
|
|
773
|
+
resource exhaustion.
|
|
774
|
+
- Task lists (`- [ ]`) render as disabled checkboxes; tables get horizontal
|
|
775
|
+
scroll containers.
|
|
776
|
+
- No change to stored content or to `/raw`.
|
|
777
|
+
|
|
778
|
+
### Tests
|
|
779
|
+
|
|
780
|
+
Highlight class assignment, Mermaid fence extraction, cap enforcement,
|
|
781
|
+
sanitization unchanged for inline HTML in Markdown.
|
|
782
|
+
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## 15. Jupyter Notebook Format
|
|
786
|
+
|
|
787
|
+
### Design
|
|
788
|
+
|
|
789
|
+
- New format `notebook` (`.ipynb`, `application/x-ipynb+json`), default
|
|
790
|
+
artifact type `analysis-report`.
|
|
791
|
+
- 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.
|
|
800
|
+
- Size guard: outputs larger than 2 MB are replaced with a "truncated" notice.
|
|
801
|
+
- `artifacty import --agent generic --file notebook.ipynb` and the browser
|
|
802
|
+
import page detect it automatically.
|
|
803
|
+
|
|
804
|
+
### Tests
|
|
805
|
+
|
|
806
|
+
Detection, cell rendering order, output MIME allowlist, oversized output
|
|
807
|
+
truncation, `/raw` fidelity.
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## 16. Structured Diff
|
|
812
|
+
|
|
813
|
+
### Design
|
|
814
|
+
|
|
815
|
+
`src/lib/diff.js` gains:
|
|
816
|
+
|
|
817
|
+
```js
|
|
818
|
+
createStructuredDiff(before, after, { format })
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
| Format | Strategy |
|
|
822
|
+
| --- | --- |
|
|
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. |
|
|
825
|
+
| `markdown`, `text`, `code`, `html` | Existing line diff plus word-level highlighting inside changed lines. |
|
|
826
|
+
| `bundle` | Per-file diff using the strategy of each file's format. |
|
|
827
|
+
|
|
828
|
+
- `/artifacts/:id/diff?from=1&to=2&view=structured|lines` picks the renderer;
|
|
829
|
+
`structured` is default for JSON-like formats.
|
|
830
|
+
- `GET /api/artifacts/:id/diff?from&to` returns the diff as JSON.
|
|
831
|
+
- MCP: `artifacty_diff` tool `{ id, from, to, view }` returns both a unified
|
|
832
|
+
text rendering in `content[].text` and the structure in `structuredContent`.
|
|
833
|
+
- Output is capped at `ARTIFACTY_MAX_DIFF_ENTRIES` (default 5000) with a
|
|
834
|
+
truncation flag.
|
|
835
|
+
|
|
836
|
+
### Tests
|
|
837
|
+
|
|
838
|
+
Key-matched array diff, positional fallback, CSV header change, word-level
|
|
839
|
+
highlight escaping, cap flag, MCP tool shape.
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## 17. Document Assets in Bundles
|
|
844
|
+
|
|
845
|
+
### Design
|
|
846
|
+
|
|
847
|
+
- Bundle file entries gain optional `contentType`. Allowed binary types are
|
|
848
|
+
extended with `application/pdf`,
|
|
849
|
+
`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,
|
|
850
|
+
`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, and
|
|
851
|
+
`application/zip`.
|
|
852
|
+
- Viewer: PDFs render in a sandboxed iframe via `/raw?file=<name>` with
|
|
853
|
+
`Content-Disposition: inline`; other document types show metadata and a
|
|
854
|
+
download link with `Content-Disposition: attachment` and `X-Content-Type-
|
|
855
|
+
Options: nosniff`.
|
|
856
|
+
- Per-file cap 32 MB and total bundle cap remains `MAX_ARTIFACT_BYTES`.
|
|
857
|
+
- Secret scanning is skipped for binary entries but file names are still
|
|
858
|
+
checked.
|
|
859
|
+
|
|
860
|
+
### Tests
|
|
861
|
+
|
|
862
|
+
Type allowlist, disposition headers, size caps, download route auth.
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## 18. CLI Watch and Diff Commands
|
|
867
|
+
|
|
868
|
+
### `artifacty watch`
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
artifacty watch --tag handoff --type artifact.updated --json
|
|
872
|
+
artifacty watch --artifact release-handoff-abc12345 --exec "./on-change.sh"
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
- Connects to `/api/events` using the URL from `server.json` or
|
|
876
|
+
`ARTIFACTY_URL`, reconnects with `Last-Event-ID` on drop.
|
|
877
|
+
- 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.
|
|
880
|
+
- `--once` exits after the first match (a shell-friendly `artifacty_wait`).
|
|
881
|
+
|
|
882
|
+
### `artifacty diff`
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
artifacty diff <id> [--from N] [--to M] [--structured] [--json]
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
Defaults `--to` to latest and `--from` to `to - 1`. Uses section 16.
|
|
889
|
+
|
|
890
|
+
### `artifacty relations`, `artifacty comment`, `artifacty retention`, `artifacty views`
|
|
891
|
+
|
|
892
|
+
Thin wrappers over the storage APIs described in their sections, following the
|
|
893
|
+
existing `cli.js` command table pattern and JSON output conventions.
|
|
894
|
+
|
|
895
|
+
### Tests
|
|
896
|
+
|
|
897
|
+
Argument parsing, reconnect logic with a mocked SSE server, `--once` exit
|
|
898
|
+
code, `--exec` environment injection.
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
## 19. OpenAPI Specification
|
|
903
|
+
|
|
904
|
+
### Design
|
|
905
|
+
|
|
906
|
+
- `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:
|
|
909
|
+
|
|
910
|
+
```js
|
|
911
|
+
{ method: "POST", path: "/api/artifacts", handler, auth: "write", summary, requestSchema, responseSchema }
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
- Served at `GET /openapi.json` (no auth) and rendered as a static reference
|
|
915
|
+
page at `/docs/api` using server-side HTML (no external UI bundle).
|
|
916
|
+
- JSON Schemas reuse `ARTIFACT_FORMATS`, `ARTIFACT_TYPES`, and the MCP tool
|
|
917
|
+
input schemas so one definition feeds both HTTP and MCP.
|
|
918
|
+
- A test asserts every registered route appears in the spec and every spec
|
|
919
|
+
path has a handler.
|
|
920
|
+
- `docs/mcp-public-api.md` links to the spec; `artifacty_info` reports the
|
|
921
|
+
URL.
|
|
922
|
+
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
## 20. MCP Protocol Refresh
|
|
926
|
+
|
|
927
|
+
### Design
|
|
928
|
+
|
|
929
|
+
- Re-verify the hand-rolled implementation against the newest MCP
|
|
930
|
+
specification before implementing sections 3 and 5. The checklist:
|
|
931
|
+
- Negotiate the newest protocol version while continuing to accept
|
|
932
|
+
`2025-06-18` from older clients.
|
|
933
|
+
- `tools/list` `outputSchema` for every tool that returns
|
|
934
|
+
`structuredContent`, generated from the same schema table as section 19.
|
|
935
|
+
- `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.
|
|
939
|
+
- Pagination cursors on `tools/list`, `resources/list`, and
|
|
940
|
+
`prompts/list` when result counts exceed 50.
|
|
941
|
+
- Streamable HTTP session resumption on `/mcp` (`Mcp-Session-Id`, event
|
|
942
|
+
replay) reusing the events table.
|
|
943
|
+
- `artifacty check` validates the negotiated version and capability set and
|
|
944
|
+
fails when a client requires a capability the server does not report.
|
|
945
|
+
- `docs/mcp-public-api.md` gains a compatibility matrix by protocol version.
|
|
946
|
+
|
|
947
|
+
### Tests
|
|
948
|
+
|
|
949
|
+
Version negotiation with old and new clients, `outputSchema` presence,
|
|
950
|
+
subscribe flow, cursor pagination, session resume through the HTTP transport.
|
|
951
|
+
|
|
952
|
+
---
|
|
953
|
+
|
|
954
|
+
## 21. Delivery Plan
|
|
955
|
+
|
|
956
|
+
| Phase | Features | Store version | Rationale |
|
|
957
|
+
| --- | --- | --- | --- |
|
|
958
|
+
| 1 | 4 Optimistic concurrency, 2 Relations, 20 MCP refresh (schema table), 19 OpenAPI | 5 | Smallest changes with the largest effect on multi-agent correctness; the shared schema table unblocks later work. |
|
|
959
|
+
| 2 | 3 Notifications, 18 CLI watch/diff, 16 Structured diff | 6 | Push channel and diff make "continue from another agent's output" real. |
|
|
960
|
+
| 3 | 10 Visibility, 11 Token scopes, 12 Rate limiting, 13 Full backups | 7 | Team-mode hardening before wider LAN use. |
|
|
961
|
+
| 4 | 5 Comments, 8 Filters and views, 9 Retention | 8 | Day-to-day usability and long-running store health. |
|
|
962
|
+
| 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
|
+
|
|
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.
|
|
968
|
+
|
|
969
|
+
### Compatibility guarantees across all phases
|
|
970
|
+
|
|
971
|
+
- Existing HTTP responses keep their current top-level keys; new keys are
|
|
972
|
+
additive.
|
|
973
|
+
- `artifacty_publish` remains an alias of `artifacty_create`.
|
|
974
|
+
- Stores without users keep single-user semantics for every new permission
|
|
975
|
+
check.
|
|
976
|
+
- `/raw` always returns the stored bytes unchanged.
|
|
977
|
+
- No generated MCP config hard-codes `http://127.0.0.1:8787`.
|