@legionworks/facet 1.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.
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +158 -0
- package/dist/gallery/chunk-4g8rem85.css +656 -0
- package/dist/gallery/chunk-c6s9k9ty.js +5970 -0
- package/dist/gallery/frame/artifact.css +8424 -0
- package/dist/gallery/frame/chunks/abnfDiagram-N423BO3Z-jj74szbm.js +155 -0
- package/dist/gallery/frame/chunks/architecture-TIHT7OUA-cvdjn7dt.js +10 -0
- package/dist/gallery/frame/chunks/architectureDiagram-T3A2C74G-n4z77eg6.js +8675 -0
- package/dist/gallery/frame/chunks/blockDiagram-VBNYF7ZC-7xsqzrd1.js +3529 -0
- package/dist/gallery/frame/chunks/c4Diagram-5PPSVZJV-z2zjs7wx.js +2413 -0
- package/dist/gallery/frame/chunks/classDiagram-JCYQIIEL-ve8rngqp.js +47 -0
- package/dist/gallery/frame/chunks/classDiagram-v2-OCEON4UE-8cfaxb8z.js +47 -0
- package/dist/gallery/frame/chunks/cose-bilkent-JH36ORCC-sn92qexx.js +4822 -0
- package/dist/gallery/frame/chunks/cynefin-VYW2F7L2-tqjv913d.js +10 -0
- package/dist/gallery/frame/chunks/cynefinDiagram-MW4NZA55-c1c0dj12.js +500 -0
- package/dist/gallery/frame/chunks/dagre-VZM6K2ZE-7tyg4yt2.js +476 -0
- package/dist/gallery/frame/chunks/diagram-7IWD3JNH-y4eng9sc.js +526 -0
- package/dist/gallery/frame/chunks/diagram-B4RE2ZJO-rn3p7j5h.js +673 -0
- package/dist/gallery/frame/chunks/diagram-LBJQPF4R-w4a2cm9q.js +255 -0
- package/dist/gallery/frame/chunks/diagram-Q27KOJAE-aqc2v7wy.js +575 -0
- package/dist/gallery/frame/chunks/diagram-UB23O5K3-xncy8nyd.js +355 -0
- package/dist/gallery/frame/chunks/ebnfDiagram-BXEA7PRR-acwgxz16.js +175 -0
- package/dist/gallery/frame/chunks/erDiagram-JOGREHBK-mzp5nswt.js +1332 -0
- package/dist/gallery/frame/chunks/eventmodeling-45OFAUF4-sg07qr0c.js +10 -0
- package/dist/gallery/frame/chunks/flowDiagram-UKHOOZJN-2e5ygs44.js +29 -0
- package/dist/gallery/frame/chunks/ganttDiagram-PKOTCBZU-p2hhh7hr.js +2620 -0
- package/dist/gallery/frame/chunks/gitGraph-TEB2WS4Q-j6kw0naa.js +10 -0
- package/dist/gallery/frame/chunks/gitGraphDiagram-DS77QQ5N-4z9z8wjm.js +1347 -0
- package/dist/gallery/frame/chunks/info-DKCQHKI2-mpt80jd3.js +10 -0
- package/dist/gallery/frame/chunks/infoDiagram-6WML65LV-5rqm3mgg.js +65 -0
- package/dist/gallery/frame/chunks/ishikawaDiagram-WSZJBQD7-b51z96jy.js +975 -0
- package/dist/gallery/frame/chunks/journeyDiagram-NVQOT4AX-9jbfp0pk.js +1272 -0
- package/dist/gallery/frame/chunks/kanban-definition-27J2QSJJ-4x0vx4se.js +1116 -0
- package/dist/gallery/frame/chunks/katex-xfgk95tv.js +14034 -0
- package/dist/gallery/frame/chunks/markdown-1s5vwkf2.js +9 -0
- package/dist/gallery/frame/chunks/markdown-2h6bwsms.js +3239 -0
- package/dist/gallery/frame/chunks/markdown-2x113yh8.js +107 -0
- package/dist/gallery/frame/chunks/markdown-30b7rwmp.js +1943 -0
- package/dist/gallery/frame/chunks/markdown-356bfj4g.js +2031 -0
- package/dist/gallery/frame/chunks/markdown-44n9ea69.js +2406 -0
- package/dist/gallery/frame/chunks/markdown-56enrf2r.js +39 -0
- package/dist/gallery/frame/chunks/markdown-5avymm8t.js +725 -0
- package/dist/gallery/frame/chunks/markdown-6r4svars.js +33 -0
- package/dist/gallery/frame/chunks/markdown-6zz8efx2.js +24 -0
- package/dist/gallery/frame/chunks/markdown-7jp6prnb.js +19 -0
- package/dist/gallery/frame/chunks/markdown-809k7q41.js +55 -0
- package/dist/gallery/frame/chunks/markdown-82xmzhzd.js +1076 -0
- package/dist/gallery/frame/chunks/markdown-92n37gd4.js +7627 -0
- package/dist/gallery/frame/chunks/markdown-ag2sssf8.js +36 -0
- package/dist/gallery/frame/chunks/markdown-b4s7ntq2.js +19 -0
- package/dist/gallery/frame/chunks/markdown-c9k0fvp1.js +100 -0
- package/dist/gallery/frame/chunks/markdown-ceefymn8.js +125 -0
- package/dist/gallery/frame/chunks/markdown-d1wfbs70.js +243 -0
- package/dist/gallery/frame/chunks/markdown-dy5sgjhc.js +11022 -0
- package/dist/gallery/frame/chunks/markdown-dz3z5w30.js +30868 -0
- package/dist/gallery/frame/chunks/markdown-eg6ngz7y.js +30340 -0
- package/dist/gallery/frame/chunks/markdown-ff5wvb43.js +1149 -0
- package/dist/gallery/frame/chunks/markdown-ffg06nfz.js +86 -0
- package/dist/gallery/frame/chunks/markdown-fvfncadf.js +472 -0
- package/dist/gallery/frame/chunks/markdown-gvyszqpv.js +19 -0
- package/dist/gallery/frame/chunks/markdown-gx1p0ew0.js +88 -0
- package/dist/gallery/frame/chunks/markdown-gxye5frz.js +65 -0
- package/dist/gallery/frame/chunks/markdown-h7pzv34e.js +105 -0
- package/dist/gallery/frame/chunks/markdown-hab789t9.js +47 -0
- package/dist/gallery/frame/chunks/markdown-hjp6sj8t.js +125 -0
- package/dist/gallery/frame/chunks/markdown-jka64qgp.js +1993 -0
- package/dist/gallery/frame/chunks/markdown-k8xkn8mw.js +56 -0
- package/dist/gallery/frame/chunks/markdown-m7frq8x8.js +112 -0
- package/dist/gallery/frame/chunks/markdown-p740v4qm.js +36 -0
- package/dist/gallery/frame/chunks/markdown-pxfeh231.js +57 -0
- package/dist/gallery/frame/chunks/markdown-pzgd8z3s.js +87 -0
- package/dist/gallery/frame/chunks/markdown-qx3cngz2.js +1673 -0
- package/dist/gallery/frame/chunks/markdown-rawhfvvc.js +85 -0
- package/dist/gallery/frame/chunks/markdown-rd8w0ng9.js +49 -0
- package/dist/gallery/frame/chunks/markdown-rm2yxfjj.js +2773 -0
- package/dist/gallery/frame/chunks/markdown-s1jatrnk.js +993 -0
- package/dist/gallery/frame/chunks/markdown-sgfg5vvq.js +59 -0
- package/dist/gallery/frame/chunks/markdown-t4ethv9d.js +409 -0
- package/dist/gallery/frame/chunks/markdown-tnmabqvz.js +36 -0
- package/dist/gallery/frame/chunks/markdown-tvdpppw8.js +5292 -0
- package/dist/gallery/frame/chunks/markdown-v0x27twr.js +22 -0
- package/dist/gallery/frame/chunks/markdown-v14td68x.js +36 -0
- package/dist/gallery/frame/chunks/markdown-w6sgd54z.js +36 -0
- package/dist/gallery/frame/chunks/markdown-xw8dsk8b.js +2125 -0
- package/dist/gallery/frame/chunks/markdown-y18mve73.js +85 -0
- package/dist/gallery/frame/chunks/markdown-yengsms5.js +427 -0
- package/dist/gallery/frame/chunks/markdown-z1wc0v0d.js +1826 -0
- package/dist/gallery/frame/chunks/markdown-zazx9h4n.js +329 -0
- package/dist/gallery/frame/chunks/mermaid-de7p4v0x.js +30 -0
- package/dist/gallery/frame/chunks/mindmap-definition-FAOFIHXS-ra44cbem.js +1216 -0
- package/dist/gallery/frame/chunks/packet-7NZHBO7P-xppm5913.js +10 -0
- package/dist/gallery/frame/chunks/pegDiagram-VL7TDLO6-a2a4wg03.js +163 -0
- package/dist/gallery/frame/chunks/pie-RZYD4A2V-hnbxk152.js +10 -0
- package/dist/gallery/frame/chunks/pieDiagram-7S7Q4E2Y-f1vxjtmx.js +311 -0
- package/dist/gallery/frame/chunks/quadrantDiagram-CIZ2JOQS-ywafa6kh.js +1390 -0
- package/dist/gallery/frame/chunks/radar-I7S5WNFK-ewqd27se.js +10 -0
- package/dist/gallery/frame/chunks/railroad-3IZDKUUU-5hy4bdx3.js +10 -0
- package/dist/gallery/frame/chunks/railroad-abnf-AHOZXSZD-bh5sxz0d.js +10 -0
- package/dist/gallery/frame/chunks/railroad-ebnf-EBAXGLYW-6b9c3zeh.js +10 -0
- package/dist/gallery/frame/chunks/railroad-peg-LSFZ7HO6-avc8v39s.js +10 -0
- package/dist/gallery/frame/chunks/railroadDiagram-AXF67PYL-qc61wax4.js +131 -0
- package/dist/gallery/frame/chunks/requirementDiagram-LRYGKXZP-9bs4786c.js +1295 -0
- package/dist/gallery/frame/chunks/sankeyDiagram-W5VNT64P-awt1em8b.js +1339 -0
- package/dist/gallery/frame/chunks/sequenceDiagram-SI44F4Z6-qaf9mz3t.js +4324 -0
- package/dist/gallery/frame/chunks/sizeCapture-X5ZJPWSS-9fq685xd.js +64 -0
- package/dist/gallery/frame/chunks/stateDiagram-OKZ733FA-vx7a7hh0.js +456 -0
- package/dist/gallery/frame/chunks/stateDiagram-v2-UEYNNEHI-x4m6ws5t.js +46 -0
- package/dist/gallery/frame/chunks/swimlanes-SLNWSIFB-e4mxm1gh.js +8287 -0
- package/dist/gallery/frame/chunks/swimlanesDiagram-ULZ7WXOC-0h93e2qq.js +42 -0
- package/dist/gallery/frame/chunks/timeline-definition-Z64GVDOM-wp0bhbzg.js +1549 -0
- package/dist/gallery/frame/chunks/treeView-QDETBFTQ-hcx7n0vt.js +10 -0
- package/dist/gallery/frame/chunks/treemap-6X3UGDF4-av02skdw.js +10 -0
- package/dist/gallery/frame/chunks/vennDiagram-T6HMQDX7-pmvk1p7n.js +2566 -0
- package/dist/gallery/frame/chunks/wardley-OPB4EBWU-njsp6e40.js +10 -0
- package/dist/gallery/frame/chunks/wardleyDiagram-T6FBY63Y-eggmnv4z.js +979 -0
- package/dist/gallery/frame/chunks/xychartDiagram-ELKLHX3M-q40tnpe5.js +1954 -0
- package/dist/gallery/frame/frame.css +183 -0
- package/dist/gallery/frame/runtime/chart.js +42744 -0
- package/dist/gallery/frame/runtime/html.js +14 -0
- package/dist/gallery/frame/runtime/markdown.js +1403 -0
- package/dist/gallery/frame/runtime/mermaid.js +32 -0
- package/dist/gallery/frame/runtime/svg.js +14 -0
- package/dist/gallery/frame/runtime/tsx.js +52 -0
- package/dist/gallery/index.html +104 -0
- package/docs/reference/cli.md +163 -0
- package/docs/reference/export.md +106 -0
- package/docs/reference/html.md +215 -0
- package/docs/reference/http.md +85 -0
- package/docs/reference/mcp.md +78 -0
- package/docs/reference/security.md +119 -0
- package/docs/reference/storage.md +93 -0
- package/docs/reference/tsx.md +101 -0
- package/docs/reference/validation.md +212 -0
- package/package.json +89 -0
- package/scripts/build-gallery.ts +127 -0
- package/scripts/launch-netns.sh +67 -0
- package/skills/facet/SKILL.md +100 -0
- package/src/cli/client.ts +311 -0
- package/src/cli/commands/create.ts +49 -0
- package/src/cli/commands/doctor.ts +249 -0
- package/src/cli/commands/export.ts +179 -0
- package/src/cli/commands/instantiate.ts +35 -0
- package/src/cli/commands/list.ts +49 -0
- package/src/cli/commands/open.ts +75 -0
- package/src/cli/commands/pin.ts +39 -0
- package/src/cli/commands/promote.ts +49 -0
- package/src/cli/commands/publish.ts +122 -0
- package/src/cli/commands/read-back.ts +56 -0
- package/src/cli/commands/status.ts +114 -0
- package/src/cli/commands/watch.ts +146 -0
- package/src/cli/main.ts +522 -0
- package/src/cli/output.ts +106 -0
- package/src/cli/parser.ts +376 -0
- package/src/cli/presenter.ts +212 -0
- package/src/cli/service-metadata.ts +134 -0
- package/src/cli/spawn-service.ts +326 -0
- package/src/css.d.ts +4 -0
- package/src/gallery-web/app.ts +1581 -0
- package/src/gallery-web/export-menu.ts +121 -0
- package/src/gallery-web/export.ts +112 -0
- package/src/gallery-web/favicon.ts +54 -0
- package/src/gallery-web/frame/entries/chart.ts +6 -0
- package/src/gallery-web/frame/entries/html.ts +6 -0
- package/src/gallery-web/frame/entries/markdown.ts +6 -0
- package/src/gallery-web/frame/entries/mermaid.ts +6 -0
- package/src/gallery-web/frame/entries/svg.ts +6 -0
- package/src/gallery-web/frame/entries/tsx.ts +6 -0
- package/src/gallery-web/frame/frame-payload.ts +91 -0
- package/src/gallery-web/frame/renderer-validation.ts +13 -0
- package/src/gallery-web/frame/renderers/chart.ts +182 -0
- package/src/gallery-web/frame/renderers/dompurify-shim.ts +112 -0
- package/src/gallery-web/frame/renderers/html.ts +48 -0
- package/src/gallery-web/frame/renderers/markdown.ts +81 -0
- package/src/gallery-web/frame/renderers/mermaid.ts +120 -0
- package/src/gallery-web/frame/renderers/registry.ts +260 -0
- package/src/gallery-web/frame/renderers/svg.ts +393 -0
- package/src/gallery-web/frame/renderers/tsx.ts +61 -0
- package/src/gallery-web/frame/runtime.ts +501 -0
- package/src/gallery-web/frame/styles/artifact.css +8424 -0
- package/src/gallery-web/frame/styles/frame.css +183 -0
- package/src/gallery-web/frame/styles/html-source.css +23 -0
- package/src/gallery-web/frame/view-box.ts +33 -0
- package/src/gallery-web/frame-html.ts +72 -0
- package/src/gallery-web/index.html +104 -0
- package/src/gallery-web/session.ts +107 -0
- package/src/gallery-web/sse-client.ts +212 -0
- package/src/gallery-web/styles/app.css +420 -0
- package/src/gallery-web/styles/tokens.css +112 -0
- package/src/gallery-web/styles/verdict.css +186 -0
- package/src/gallery-web/swap.ts +64 -0
- package/src/gallery-web/theme.ts +40 -0
- package/src/gallery-web/view-state.ts +93 -0
- package/src/harness-adapters/claude-code/README.md +10 -0
- package/src/harness-adapters/claude-code/facet.sh +4 -0
- package/src/harness-adapters/codex/README.md +10 -0
- package/src/harness-adapters/codex/facet.sh +4 -0
- package/src/harness-adapters/mcp/README.md +13 -0
- package/src/harness-adapters/mcp/cli-bridge.ts +216 -0
- package/src/harness-adapters/mcp/main.ts +15 -0
- package/src/harness-adapters/mcp/server.ts +164 -0
- package/src/harness-adapters/mcp/tool-schemas.ts +53 -0
- package/src/harness-adapters/opencode/README.md +10 -0
- package/src/harness-adapters/opencode/facet.sh +4 -0
- package/src/runtime/compiled-entrypoints.ts +69 -0
- package/src/service/dispatcher.ts +717 -0
- package/src/service/export.ts +218 -0
- package/src/service/http-utils.ts +55 -0
- package/src/service/lexical/expectations.ts +210 -0
- package/src/service/lifecycle/idle-controller.ts +125 -0
- package/src/service/lifecycle/orphan-cleanup.ts +128 -0
- package/src/service/lifecycle/process-lock.ts +235 -0
- package/src/service/main.ts +136 -0
- package/src/service/process.ts +240 -0
- package/src/service/router-guards.ts +239 -0
- package/src/service/router.ts +627 -0
- package/src/service/security/auth.ts +168 -0
- package/src/service/security/host-origin.ts +138 -0
- package/src/service/security/http-guards.ts +13 -0
- package/src/service/security/leases.ts +182 -0
- package/src/service/security/token-store.ts +138 -0
- package/src/service/server.ts +356 -0
- package/src/service/store/database.ts +61 -0
- package/src/service/store/evidence-retention.ts +234 -0
- package/src/service/store/migrations.ts +167 -0
- package/src/service/store/repository-lifecycle.ts +121 -0
- package/src/service/store/repository.ts +732 -0
- package/src/service/store/schema.ts +218 -0
- package/src/service/stored-verdict.ts +97 -0
- package/src/service/stream.ts +301 -0
- package/src/service/verdict-enrichment.ts +45 -0
- package/src/shared/build/frame-bundle-plugins.ts +61 -0
- package/src/shared/build-mode.ts +28 -0
- package/src/shared/config/evidence-read.ts +98 -0
- package/src/shared/config/limits.ts +56 -0
- package/src/shared/config/paths.ts +96 -0
- package/src/shared/contracts/artifact-types.ts +2 -0
- package/src/shared/contracts/artifact.ts +98 -0
- package/src/shared/contracts/commands/_shared.ts +59 -0
- package/src/shared/contracts/commands/guards.ts +86 -0
- package/src/shared/contracts/commands/index.ts +204 -0
- package/src/shared/contracts/commands/names.ts +35 -0
- package/src/shared/contracts/commands/requests.ts +149 -0
- package/src/shared/contracts/commands/results.ts +200 -0
- package/src/shared/contracts/envelope.ts +140 -0
- package/src/shared/contracts/events.ts +41 -0
- package/src/shared/contracts/observed-counts.ts +19 -0
- package/src/shared/contracts/renderers.ts +6 -0
- package/src/shared/contracts/validation.ts +296 -0
- package/src/shared/errors/facet-error.ts +127 -0
- package/src/shared/errors/store-error.ts +68 -0
- package/src/shared/evidence-image.ts +37 -0
- package/src/shared/export.ts +31 -0
- package/src/shared/html/policy.ts +108 -0
- package/src/shared/html/style-vocabulary.ts +352 -0
- package/src/shared/logging/logger.ts +108 -0
- package/src/shared/logging/redact.ts +49 -0
- package/src/shared/security/frozen-csp.ts +13 -0
- package/src/shared/storage-version.ts +2 -0
- package/src/shared/tsx/execution.ts +29 -0
- package/src/shared/tsx/import-policy.ts +94 -0
- package/src/shared/util/dir-permissions.ts +94 -0
- package/src/shared/util/mermaid-nodes.ts +230 -0
- package/src/shared/util/process.ts +67 -0
- package/src/shared/util/time.ts +21 -0
- package/src/shared/version.ts +3 -0
- package/src/validation/sandbox/limits.ts +41 -0
- package/src/validation/sandbox/netns.ts +177 -0
- package/src/validation/tier0/chart.ts +177 -0
- package/src/validation/tier0/dom-shim.ts +54 -0
- package/src/validation/tier0/html.ts +408 -0
- package/src/validation/tier0/markdown.ts +285 -0
- package/src/validation/tier0/mermaid.ts +98 -0
- package/src/validation/tier0/runner.ts +504 -0
- package/src/validation/tier0/svg.ts +321 -0
- package/src/validation/tier0/tsx/allowlist-resolver.ts +77 -0
- package/src/validation/tier0/tsx/ast-policy-capabilities.ts +238 -0
- package/src/validation/tier0/tsx/ast-policy-shared.ts +71 -0
- package/src/validation/tier0/tsx/ast-policy.ts +181 -0
- package/src/validation/tier0/tsx/compiler.ts +156 -0
- package/src/validation/tier0/worker-dispatch.ts +211 -0
- package/src/validation/tier0/worker-entry.ts +96 -0
- package/src/validation/tier0/worker-input.ts +75 -0
- package/src/validation/tier1/browser-process.ts +147 -0
- package/src/validation/tier1/cdp-pipe.ts +281 -0
- package/src/validation/tier1/entries/chart.ts +5 -0
- package/src/validation/tier1/entries/html.ts +6 -0
- package/src/validation/tier1/entries/markdown.ts +5 -0
- package/src/validation/tier1/entries/mermaid.ts +5 -0
- package/src/validation/tier1/entries/svg.ts +5 -0
- package/src/validation/tier1/entries/tsx.ts +5 -0
- package/src/validation/tier1/frame-target.ts +162 -0
- package/src/validation/tier1/harness-entry.ts +261 -0
- package/src/validation/tier1/harness.ts +215 -0
- package/src/validation/tier1/isolated-probe.ts +94 -0
- package/src/validation/tier1/launcher.ts +177 -0
- package/src/validation/tier1/limits.ts +80 -0
- package/src/validation/tier1/nonce.ts +17 -0
- package/src/validation/tier1/protocol-probe.ts +519 -0
- package/src/validation/tier1/runner.ts +1122 -0
- package/src/validation/tier1/verdict.ts +246 -0
- package/src/validation/tier1/webp.ts +69 -0
- package/templates/README.md +88 -0
- package/templates/bar-compare.vl.json +63 -0
- package/templates/capacity-report.tsx +102 -0
- package/templates/decision-record.md +24 -0
- package/templates/deployment-state.mmd +40 -0
- package/templates/exemplar.md +91 -0
- package/templates/facet-story.tsx +799 -0
- package/templates/fleet-dashboard.html +141 -0
- package/templates/html-release-ledger.html +91 -0
- package/templates/html-status-report.html +114 -0
- package/templates/incident-console.tsx +147 -0
- package/templates/legion-boundaries.mmd +23 -0
- package/templates/legion-flow.mmd +15 -0
- package/templates/legion-sequence.mmd +14 -0
- package/templates/legion-state.mmd +7 -0
- package/templates/metric-card.svg +29 -0
- package/templates/observability-map.svg +101 -0
- package/templates/pipeline-audit.md +108 -0
- package/templates/release-metrics.vl.json +98 -0
- package/templates/service-topology.mmd +47 -0
- package/templates/status-report.md +32 -0
- package/templates/system-map.svg +53 -0
- package/templates/timeseries.vl.json +71 -0
- package/templates/tsx-interactive-counter.tsx +19 -0
- package/templates/tsx-status-report.tsx +31 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import {
|
|
3
|
+
ArtifactSchema,
|
|
4
|
+
ArtifactTypeSchema,
|
|
5
|
+
ProjectSchema,
|
|
6
|
+
RenderRunSchema,
|
|
7
|
+
RevisionSchema,
|
|
8
|
+
TemplateSchema,
|
|
9
|
+
type Artifact,
|
|
10
|
+
type ArtifactType,
|
|
11
|
+
type Renderer,
|
|
12
|
+
type Project,
|
|
13
|
+
type RenderRun,
|
|
14
|
+
type Revision,
|
|
15
|
+
type Template,
|
|
16
|
+
} from "../../shared/contracts/artifact";
|
|
17
|
+
import type {
|
|
18
|
+
InsecureMarker,
|
|
19
|
+
ScreenshotError,
|
|
20
|
+
TsxExecutionMode,
|
|
21
|
+
} from "../../shared/contracts/validation";
|
|
22
|
+
import type { EvidenceImageFormat } from "../../shared/evidence-image";
|
|
23
|
+
import { DEFAULT_LIST_LIMIT } from "../../shared/config/limits";
|
|
24
|
+
import { now } from "../../shared/util/time";
|
|
25
|
+
import { asStoreError, FacetStoreError, hardenDatabaseFiles } from "./database";
|
|
26
|
+
import {
|
|
27
|
+
evictRevisions,
|
|
28
|
+
createTemplate as createLifecycleTemplate,
|
|
29
|
+
pinRevision as pinLifecycleRevision,
|
|
30
|
+
promoteRevision as promoteLifecycleRevision,
|
|
31
|
+
type PromoteRevisionInput,
|
|
32
|
+
type TemplateInput,
|
|
33
|
+
} from "./repository-lifecycle";
|
|
34
|
+
import { enforceEvidenceRetention, removeUnreferencedEvidence } from "./evidence-retention";
|
|
35
|
+
|
|
36
|
+
interface ProjectInput {
|
|
37
|
+
readonly projectRoot: string;
|
|
38
|
+
}
|
|
39
|
+
interface ArtifactInput {
|
|
40
|
+
readonly projectId: string;
|
|
41
|
+
readonly slug: string;
|
|
42
|
+
readonly title: string;
|
|
43
|
+
}
|
|
44
|
+
interface PublishInput {
|
|
45
|
+
readonly artifactId: string;
|
|
46
|
+
readonly artifactType: ArtifactType | "html";
|
|
47
|
+
readonly renderer?: Renderer;
|
|
48
|
+
readonly source: Uint8Array;
|
|
49
|
+
readonly note?: string | null;
|
|
50
|
+
readonly parentRevisionId?: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* TSX execution mode (D2). Optional — defaults to `'static'` for
|
|
53
|
+
* every artifact type on disk so non-TSX rows carry the canonical
|
|
54
|
+
* value and the wire form simply omits it. Required to be `'static'`
|
|
55
|
+
* when `artifactType !== 'tsx'`; the dispatcher guard ensures this
|
|
56
|
+
* before the row is written. Derived from `TsxExecutionMode` so a
|
|
57
|
+
* new mode lands in one place.
|
|
58
|
+
*/
|
|
59
|
+
readonly execution?: TsxExecutionMode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface RenderRunInput {
|
|
63
|
+
readonly revisionId: string;
|
|
64
|
+
readonly tier: 0 | 1;
|
|
65
|
+
readonly status: string;
|
|
66
|
+
readonly expected: unknown;
|
|
67
|
+
readonly observed: unknown;
|
|
68
|
+
readonly screenshotPath?: string | null;
|
|
69
|
+
readonly screenshotFormat?: EvidenceImageFormat | null;
|
|
70
|
+
readonly consolePath?: string | null;
|
|
71
|
+
readonly screenshotError?: ScreenshotError | null;
|
|
72
|
+
readonly insecure?: InsecureMarker | null;
|
|
73
|
+
/**
|
|
74
|
+
* TSX compiled-bundle evidence (D7). The bundle is derived output
|
|
75
|
+
* stored alongside the run, not a wire form — `null` (the default)
|
|
76
|
+
* for non-TSX runs and for TSX runs whose compilation did not
|
|
77
|
+
* produce a retained file. Retention cleanup deletes the file
|
|
78
|
+
* alongside its row.
|
|
79
|
+
*/
|
|
80
|
+
readonly compiledPath?: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Retained-evidence carve-out: `true` exempts the row from the
|
|
83
|
+
* last-N cleanup policy. Pin/template call sites set this; the
|
|
84
|
+
* default false makes new runs eviction-eligible.
|
|
85
|
+
*/
|
|
86
|
+
readonly retained?: boolean;
|
|
87
|
+
readonly startedAt?: string;
|
|
88
|
+
readonly finishedAt?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface WriteHookContext {
|
|
92
|
+
readonly phase: "after_insert" | "before_commit";
|
|
93
|
+
}
|
|
94
|
+
interface RepositoryOptions {
|
|
95
|
+
readonly onCommitted?: (revision: Revision) => void;
|
|
96
|
+
readonly writeHook?: (context: WriteHookContext) => void;
|
|
97
|
+
/**
|
|
98
|
+
* Evidence root used by the last-N retention cleanup. When omitted,
|
|
99
|
+
* retention is skipped (acceptable for in-process / unit tests that
|
|
100
|
+
* never write evidence files).
|
|
101
|
+
*/
|
|
102
|
+
readonly evidenceRoot?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Legacy evidence root (pre explicit-threading child-derived root). Read
|
|
105
|
+
* only — never written by retention. The export path consults it as a
|
|
106
|
+
* tolerant fallback for old evidence.
|
|
107
|
+
*/
|
|
108
|
+
readonly legacyEvidenceRoot?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface ListArtifactsInput {
|
|
112
|
+
readonly projectId: string;
|
|
113
|
+
readonly slugPrefix?: string;
|
|
114
|
+
readonly limit?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface StatusForArtifactInput {
|
|
118
|
+
readonly artifactId: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type SqlRevision = Omit<
|
|
122
|
+
Revision,
|
|
123
|
+
| "artifactId"
|
|
124
|
+
| "revisionNumber"
|
|
125
|
+
| "parentRevisionId"
|
|
126
|
+
| "artifactType"
|
|
127
|
+
| "createdAt"
|
|
128
|
+
| "pinned"
|
|
129
|
+
| "source"
|
|
130
|
+
| "execution"
|
|
131
|
+
> & {
|
|
132
|
+
artifact_id: string;
|
|
133
|
+
revision_number: number;
|
|
134
|
+
parent_revision_id: string | null;
|
|
135
|
+
artifact_type: string;
|
|
136
|
+
renderer: string;
|
|
137
|
+
source: Uint8Array | ArrayBuffer;
|
|
138
|
+
note: string | null;
|
|
139
|
+
pinned: number;
|
|
140
|
+
created_at: string;
|
|
141
|
+
execution: TsxExecutionMode;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function sha256(source: Uint8Array): string {
|
|
145
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
146
|
+
hasher.update(source);
|
|
147
|
+
return hasher.digest("hex");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseArtifactType(value: ArtifactType): ArtifactType {
|
|
151
|
+
try {
|
|
152
|
+
return ArtifactTypeSchema.parse(value);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new FacetStoreError("invalid_artifact_type", "Unsupported artifact type", {
|
|
155
|
+
cause: error,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function bytes(value: Uint8Array | ArrayBuffer): Uint8Array {
|
|
161
|
+
return value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function revisionHasColumn(db: Database, name: string): boolean {
|
|
165
|
+
const rows = db.query(`PRAGMA table_info(revisions)`).all() as Array<{ name: string }>;
|
|
166
|
+
return rows.some((row) => row.name === name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderRunHasColumn(db: Database, name: string): boolean {
|
|
170
|
+
const rows = db.query(`PRAGMA table_info(render_runs)`).all() as Array<{ name: string }>;
|
|
171
|
+
return rows.some((row) => row.name === name);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function mapRevision(row: SqlRevision): Revision {
|
|
175
|
+
// TSX carries `execution` on the wire; every other type carries it
|
|
176
|
+
// on disk but the wire form omits the field (the dispatch is
|
|
177
|
+
// explicit: `execution` must be absent, not null, for non-TSX).
|
|
178
|
+
return RevisionSchema.parse({
|
|
179
|
+
id: row.id,
|
|
180
|
+
artifactId: row.artifact_id,
|
|
181
|
+
revisionNumber: row.revision_number,
|
|
182
|
+
parentRevisionId: row.parent_revision_id,
|
|
183
|
+
artifactType: row.artifact_type,
|
|
184
|
+
renderer: row.renderer,
|
|
185
|
+
source: bytes(row.source),
|
|
186
|
+
sha256: row.sha256,
|
|
187
|
+
note: row.note,
|
|
188
|
+
pinned: row.pinned === 1,
|
|
189
|
+
createdAt: row.created_at,
|
|
190
|
+
...(row.artifact_type === "tsx" ? { execution: row.execution } : {}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export class ArtifactRepository {
|
|
195
|
+
constructor(
|
|
196
|
+
private readonly db: Database,
|
|
197
|
+
private readonly options: RepositoryOptions = {},
|
|
198
|
+
) {}
|
|
199
|
+
|
|
200
|
+
getEvidenceRoot(): string | undefined {
|
|
201
|
+
return this.options.evidenceRoot;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getLegacyEvidenceRoot(): string | undefined {
|
|
205
|
+
return this.options.legacyEvidenceRoot;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
createProject(input: ProjectInput): Project {
|
|
209
|
+
const value = { id: crypto.randomUUID(), projectRoot: input.projectRoot, createdAt: now() };
|
|
210
|
+
try {
|
|
211
|
+
this.db
|
|
212
|
+
.query("INSERT INTO projects(id, project_root, created_at) VALUES (?, ?, ?)")
|
|
213
|
+
.run(value.id, value.projectRoot, value.createdAt);
|
|
214
|
+
return ProjectSchema.parse(value);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw asStoreError(error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getProjectById(id: string): Project | null {
|
|
221
|
+
try {
|
|
222
|
+
const row = this.db
|
|
223
|
+
.query("SELECT id, project_root, created_at FROM projects WHERE id = ?")
|
|
224
|
+
.get(id) as { id: string; project_root: string; created_at: string } | null;
|
|
225
|
+
if (!row) return null;
|
|
226
|
+
return ProjectSchema.parse({
|
|
227
|
+
id: row.id,
|
|
228
|
+
projectRoot: row.project_root,
|
|
229
|
+
createdAt: row.created_at,
|
|
230
|
+
});
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw asStoreError(error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Look up by id, creating the project on demand. The first lookup
|
|
238
|
+
* for a given id stamps it with the supplied root; subsequent lookups
|
|
239
|
+
* return the existing row. The synthesized root `${root}-${id}` keeps
|
|
240
|
+
* the `project_root` UNIQUE constraint satisfied while preserving
|
|
241
|
+
* the caller-chosen id for stable addressing.
|
|
242
|
+
*/
|
|
243
|
+
getOrCreateProjectById(id: string, projectRoot: string): Project {
|
|
244
|
+
const existing = this.getProjectById(id);
|
|
245
|
+
if (existing) return existing;
|
|
246
|
+
const value = { id, projectRoot: `${projectRoot}-${id}`, createdAt: now() };
|
|
247
|
+
try {
|
|
248
|
+
this.db
|
|
249
|
+
.query("INSERT INTO projects(id, project_root, created_at) VALUES (?, ?, ?)")
|
|
250
|
+
.run(value.id, value.projectRoot, value.createdAt);
|
|
251
|
+
return ProjectSchema.parse(value);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw asStoreError(error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
listArtifacts(input: ListArtifactsInput): Artifact[] {
|
|
258
|
+
const limit = input.limit ?? DEFAULT_LIST_LIMIT;
|
|
259
|
+
const prefix = input.slugPrefix ?? "";
|
|
260
|
+
try {
|
|
261
|
+
const rows = this.db
|
|
262
|
+
.query(
|
|
263
|
+
"SELECT id, project_id, slug, title, created_at, updated_at FROM artifacts WHERE project_id = ? AND slug LIKE ? ORDER BY created_at DESC LIMIT ?",
|
|
264
|
+
)
|
|
265
|
+
.all(input.projectId, `${prefix}%`, limit) as Array<{
|
|
266
|
+
id: string;
|
|
267
|
+
project_id: string;
|
|
268
|
+
slug: string;
|
|
269
|
+
title: string;
|
|
270
|
+
created_at: string;
|
|
271
|
+
updated_at: string;
|
|
272
|
+
}>;
|
|
273
|
+
return rows.map((row) =>
|
|
274
|
+
ArtifactSchema.parse({
|
|
275
|
+
id: row.id,
|
|
276
|
+
projectId: row.project_id,
|
|
277
|
+
slug: row.slug,
|
|
278
|
+
title: row.title,
|
|
279
|
+
createdAt: row.created_at,
|
|
280
|
+
updatedAt: row.updated_at,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw asStoreError(error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getArtifactById(id: string): Artifact | null {
|
|
289
|
+
try {
|
|
290
|
+
const row = this.db
|
|
291
|
+
.query(
|
|
292
|
+
"SELECT id, project_id, slug, title, created_at, updated_at FROM artifacts WHERE id = ?",
|
|
293
|
+
)
|
|
294
|
+
.get(id) as {
|
|
295
|
+
id: string;
|
|
296
|
+
project_id: string;
|
|
297
|
+
slug: string;
|
|
298
|
+
title: string;
|
|
299
|
+
created_at: string;
|
|
300
|
+
updated_at: string;
|
|
301
|
+
} | null;
|
|
302
|
+
if (row === null) return null;
|
|
303
|
+
return ArtifactSchema.parse({
|
|
304
|
+
id: row.id,
|
|
305
|
+
projectId: row.project_id,
|
|
306
|
+
slug: row.slug,
|
|
307
|
+
title: row.title,
|
|
308
|
+
createdAt: row.created_at,
|
|
309
|
+
updatedAt: row.updated_at,
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
throw asStoreError(error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
listRenderRuns(input: { revisionId: string; tier: 0 | 1 }): RenderRun[] {
|
|
317
|
+
const hasCompiledPath = renderRunHasColumn(this.db, "compiled_path");
|
|
318
|
+
const hasScreenshotFormat = renderRunHasColumn(this.db, "screenshot_format");
|
|
319
|
+
try {
|
|
320
|
+
const columns = [
|
|
321
|
+
"id",
|
|
322
|
+
"revision_id",
|
|
323
|
+
"tier",
|
|
324
|
+
"status",
|
|
325
|
+
"expected_json",
|
|
326
|
+
"observed_json",
|
|
327
|
+
"screenshot_path",
|
|
328
|
+
...(hasScreenshotFormat ? ["screenshot_format"] : []),
|
|
329
|
+
"console_path",
|
|
330
|
+
"screenshot_error_json",
|
|
331
|
+
"insecure_json",
|
|
332
|
+
"retained",
|
|
333
|
+
...(hasCompiledPath ? ["compiled_path"] : []),
|
|
334
|
+
"started_at",
|
|
335
|
+
"finished_at",
|
|
336
|
+
];
|
|
337
|
+
const selectSql = `SELECT ${columns.join(", ")} FROM render_runs WHERE revision_id = ? AND tier = ? ORDER BY finished_at DESC`;
|
|
338
|
+
const rows = this.db.query(selectSql).all(input.revisionId, input.tier) as Array<{
|
|
339
|
+
id: string;
|
|
340
|
+
revision_id: string;
|
|
341
|
+
tier: number;
|
|
342
|
+
status: string;
|
|
343
|
+
expected_json: string;
|
|
344
|
+
observed_json: string;
|
|
345
|
+
screenshot_path: string | null;
|
|
346
|
+
screenshot_format?: EvidenceImageFormat | null;
|
|
347
|
+
console_path: string | null;
|
|
348
|
+
screenshot_error_json: string | null;
|
|
349
|
+
insecure_json: string | null;
|
|
350
|
+
retained: number;
|
|
351
|
+
compiled_path?: string | null;
|
|
352
|
+
started_at: string;
|
|
353
|
+
finished_at: string;
|
|
354
|
+
}>;
|
|
355
|
+
return rows.map((row) =>
|
|
356
|
+
RenderRunSchema.parse({
|
|
357
|
+
id: row.id,
|
|
358
|
+
revisionId: row.revision_id,
|
|
359
|
+
tier: row.tier,
|
|
360
|
+
status: row.status,
|
|
361
|
+
expectedJson: row.expected_json,
|
|
362
|
+
observedJson: row.observed_json,
|
|
363
|
+
screenshotPath: row.screenshot_path,
|
|
364
|
+
screenshotFormat: hasScreenshotFormat ? (row.screenshot_format ?? null) : null,
|
|
365
|
+
consolePath: row.console_path,
|
|
366
|
+
screenshotErrorJson: row.screenshot_error_json,
|
|
367
|
+
insecureJson: row.insecure_json,
|
|
368
|
+
retained: row.retained === 1,
|
|
369
|
+
compiledPath: hasCompiledPath ? (row.compiled_path ?? null) : null,
|
|
370
|
+
startedAt: row.started_at,
|
|
371
|
+
finishedAt: row.finished_at,
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw asStoreError(error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
statusForArtifact(input: StatusForArtifactInput): {
|
|
380
|
+
revisionCount: number;
|
|
381
|
+
pinnedCount: number;
|
|
382
|
+
templateCount: number;
|
|
383
|
+
} {
|
|
384
|
+
try {
|
|
385
|
+
const revisionRow = this.db
|
|
386
|
+
.query("SELECT COUNT(*) AS count FROM revisions WHERE artifact_id = ?")
|
|
387
|
+
.get(input.artifactId) as { count: number };
|
|
388
|
+
const pinnedRow = this.db
|
|
389
|
+
.query("SELECT COUNT(*) AS count FROM revisions WHERE artifact_id = ? AND pinned = 1")
|
|
390
|
+
.get(input.artifactId) as { count: number };
|
|
391
|
+
const templateRow = this.db
|
|
392
|
+
.query(
|
|
393
|
+
"SELECT COUNT(*) AS count FROM templates t JOIN revisions r ON r.id = t.revision_id WHERE r.artifact_id = ?",
|
|
394
|
+
)
|
|
395
|
+
.get(input.artifactId) as { count: number };
|
|
396
|
+
return {
|
|
397
|
+
revisionCount: revisionRow.count,
|
|
398
|
+
pinnedCount: pinnedRow.count,
|
|
399
|
+
templateCount: templateRow.count,
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw asStoreError(error);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
findTemplateByName(name: string): Template | null {
|
|
407
|
+
try {
|
|
408
|
+
const row = this.db
|
|
409
|
+
.query(
|
|
410
|
+
"SELECT id, artifact_id, revision_id, name, description, promoted_by, promoted_at FROM templates WHERE name = ? ORDER BY promoted_at DESC LIMIT 1",
|
|
411
|
+
)
|
|
412
|
+
.get(name) as {
|
|
413
|
+
id: string;
|
|
414
|
+
artifact_id: string;
|
|
415
|
+
revision_id: string;
|
|
416
|
+
name: string;
|
|
417
|
+
description: string | null;
|
|
418
|
+
promoted_by: string;
|
|
419
|
+
promoted_at: string;
|
|
420
|
+
} | null;
|
|
421
|
+
if (!row) return null;
|
|
422
|
+
return TemplateSchema.parse({
|
|
423
|
+
id: row.id,
|
|
424
|
+
artifactId: row.artifact_id,
|
|
425
|
+
revisionId: row.revision_id,
|
|
426
|
+
name: row.name,
|
|
427
|
+
description: row.description,
|
|
428
|
+
promotedBy: row.promoted_by,
|
|
429
|
+
promotedAt: row.promoted_at,
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw asStoreError(error);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
createArtifact(input: ArtifactInput): Artifact {
|
|
437
|
+
const createdAt = now();
|
|
438
|
+
const value = { id: crypto.randomUUID(), ...input, createdAt, updatedAt: createdAt };
|
|
439
|
+
try {
|
|
440
|
+
this.db
|
|
441
|
+
.query(
|
|
442
|
+
"INSERT INTO artifacts(id, project_id, slug, title, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
|
443
|
+
)
|
|
444
|
+
.run(value.id, value.projectId, value.slug, value.title, value.createdAt, value.updatedAt);
|
|
445
|
+
return ArtifactSchema.parse(value);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
throw asStoreError(error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
publishRevision(input: PublishInput): Revision {
|
|
452
|
+
parseArtifactType(input.artifactType);
|
|
453
|
+
const source = new Uint8Array(input.source);
|
|
454
|
+
const revisionId = crypto.randomUUID();
|
|
455
|
+
const timestamp = now();
|
|
456
|
+
const sha = sha256(source);
|
|
457
|
+
// D2: every revision carries an execution mode on disk. Non-TSX
|
|
458
|
+
// rows store `'static'` (the canonical default) and the wire
|
|
459
|
+
// envelope omits the field; TSX rows carry the declared value
|
|
460
|
+
// end-to-end.
|
|
461
|
+
const execution = input.execution ?? "static";
|
|
462
|
+
// The `execution` column lands in the v8 migration. Detect it
|
|
463
|
+
// here so the v6-rollback test (which publishes against a v5
|
|
464
|
+
// schema) keeps its semantics: the rollback assertion is about
|
|
465
|
+
// the v6 transaction, not about whether the publish path itself
|
|
466
|
+
// needed v8.
|
|
467
|
+
const hasExecutionColumn = revisionHasColumn(this.db, "execution");
|
|
468
|
+
try {
|
|
469
|
+
const transact = this.db.transaction(() => {
|
|
470
|
+
const previous = this.db
|
|
471
|
+
.query(
|
|
472
|
+
"SELECT id, revision_number FROM revisions WHERE artifact_id = ? ORDER BY revision_number DESC LIMIT 1",
|
|
473
|
+
)
|
|
474
|
+
.get(input.artifactId) as { id: string; revision_number: number } | null;
|
|
475
|
+
const revisionNumber = (previous?.revision_number ?? 0) + 1;
|
|
476
|
+
if (hasExecutionColumn) {
|
|
477
|
+
this.db
|
|
478
|
+
.query(
|
|
479
|
+
"INSERT INTO revisions(id, artifact_id, revision_number, parent_revision_id, artifact_type, renderer, source, sha256, note, created_at, execution) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
480
|
+
)
|
|
481
|
+
.run(
|
|
482
|
+
revisionId,
|
|
483
|
+
input.artifactId,
|
|
484
|
+
revisionNumber,
|
|
485
|
+
input.parentRevisionId === undefined
|
|
486
|
+
? (previous?.id ?? null)
|
|
487
|
+
: input.parentRevisionId,
|
|
488
|
+
input.artifactType,
|
|
489
|
+
input.renderer ?? "svg",
|
|
490
|
+
source,
|
|
491
|
+
sha,
|
|
492
|
+
input.note ?? null,
|
|
493
|
+
timestamp,
|
|
494
|
+
execution,
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
this.db
|
|
498
|
+
.query(
|
|
499
|
+
"INSERT INTO revisions(id, artifact_id, revision_number, parent_revision_id, artifact_type, renderer, source, sha256, note, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
500
|
+
)
|
|
501
|
+
.run(
|
|
502
|
+
revisionId,
|
|
503
|
+
input.artifactId,
|
|
504
|
+
revisionNumber,
|
|
505
|
+
input.parentRevisionId === undefined
|
|
506
|
+
? (previous?.id ?? null)
|
|
507
|
+
: input.parentRevisionId,
|
|
508
|
+
input.artifactType,
|
|
509
|
+
input.renderer ?? "svg",
|
|
510
|
+
source,
|
|
511
|
+
sha,
|
|
512
|
+
input.note ?? null,
|
|
513
|
+
timestamp,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const revision = RevisionSchema.parse({
|
|
517
|
+
id: revisionId,
|
|
518
|
+
artifactId: input.artifactId,
|
|
519
|
+
revisionNumber,
|
|
520
|
+
parentRevisionId:
|
|
521
|
+
input.parentRevisionId === undefined ? (previous?.id ?? null) : input.parentRevisionId,
|
|
522
|
+
artifactType: input.artifactType,
|
|
523
|
+
renderer: input.renderer ?? "svg",
|
|
524
|
+
source,
|
|
525
|
+
sha256: sha,
|
|
526
|
+
note: input.note ?? null,
|
|
527
|
+
pinned: false,
|
|
528
|
+
createdAt: timestamp,
|
|
529
|
+
...(input.artifactType === "tsx" && hasExecutionColumn ? { execution } : {}),
|
|
530
|
+
});
|
|
531
|
+
this.options.writeHook?.({ phase: "after_insert" });
|
|
532
|
+
evictRevisions(this.db, input.artifactId, revisionId);
|
|
533
|
+
|
|
534
|
+
this.options.writeHook?.({ phase: "before_commit" });
|
|
535
|
+
return revision;
|
|
536
|
+
});
|
|
537
|
+
const revision = transact.immediate();
|
|
538
|
+
hardenDatabaseFiles(this.db.filename);
|
|
539
|
+
this.options.onCommitted?.(revision);
|
|
540
|
+
return revision;
|
|
541
|
+
} catch (error) {
|
|
542
|
+
const mapped = asStoreError(error);
|
|
543
|
+
if (mapped.code === "duplicate_revision") {
|
|
544
|
+
throw new FacetStoreError("duplicate_revision", mapped.message, {
|
|
545
|
+
cause: error,
|
|
546
|
+
details: { revisionSha: sha },
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
throw mapped;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
getRevisionBySha(artifactId: string, revisionSha: string): Revision | null {
|
|
554
|
+
try {
|
|
555
|
+
const row = this.db
|
|
556
|
+
.query("SELECT * FROM revisions WHERE artifact_id = ? AND sha256 = ?")
|
|
557
|
+
.get(artifactId, revisionSha) as SqlRevision | null;
|
|
558
|
+
return row ? mapRevision(row) : null;
|
|
559
|
+
} catch (error) {
|
|
560
|
+
throw asStoreError(error);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
getLatestRevision(artifactId: string): Revision | null {
|
|
565
|
+
try {
|
|
566
|
+
const row = this.db
|
|
567
|
+
.query(
|
|
568
|
+
"SELECT * FROM revisions WHERE artifact_id = ? ORDER BY revision_number DESC LIMIT 1",
|
|
569
|
+
)
|
|
570
|
+
.get(artifactId) as SqlRevision | null;
|
|
571
|
+
return row === null ? null : mapRevision(row);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
throw asStoreError(error);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
getRevisionById(revisionId: string): Revision | null {
|
|
578
|
+
try {
|
|
579
|
+
const row = this.db
|
|
580
|
+
.query("SELECT * FROM revisions WHERE id = ?")
|
|
581
|
+
.get(revisionId) as SqlRevision | null;
|
|
582
|
+
return row ? mapRevision(row) : null;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
throw asStoreError(error);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
recordRenderRun(input: RenderRunInput): RenderRun {
|
|
589
|
+
const startedAt = input.startedAt ?? now();
|
|
590
|
+
const finishedAt = input.finishedAt ?? now();
|
|
591
|
+
const retained = input.retained ?? false;
|
|
592
|
+
const compiledPath = input.compiledPath ?? null;
|
|
593
|
+
const screenshotFormat = input.screenshotFormat ?? null;
|
|
594
|
+
// The `compiled_path` column lands in the v8 migration. Detect it
|
|
595
|
+
// so the v6-rollback test (which records runs against a v5
|
|
596
|
+
// schema) keeps its semantics: the rollback assertion is about
|
|
597
|
+
// the v6 transaction, not about whether the record path itself
|
|
598
|
+
// needed v8.
|
|
599
|
+
const hasCompiledPath = renderRunHasColumn(this.db, "compiled_path");
|
|
600
|
+
const hasScreenshotFormat = renderRunHasColumn(this.db, "screenshot_format");
|
|
601
|
+
const value: {
|
|
602
|
+
id: string;
|
|
603
|
+
revisionId: string;
|
|
604
|
+
tier: 0 | 1;
|
|
605
|
+
status: string;
|
|
606
|
+
expectedJson: string;
|
|
607
|
+
observedJson: string;
|
|
608
|
+
screenshotPath: string | null;
|
|
609
|
+
screenshotFormat: EvidenceImageFormat | null;
|
|
610
|
+
consolePath: string | null;
|
|
611
|
+
screenshotErrorJson: string | null;
|
|
612
|
+
insecureJson: string | null;
|
|
613
|
+
retained: boolean;
|
|
614
|
+
compiledPath?: string | null;
|
|
615
|
+
startedAt: string;
|
|
616
|
+
finishedAt: string;
|
|
617
|
+
} = {
|
|
618
|
+
id: crypto.randomUUID(),
|
|
619
|
+
revisionId: input.revisionId,
|
|
620
|
+
tier: input.tier,
|
|
621
|
+
status: input.status,
|
|
622
|
+
expectedJson: JSON.stringify(input.expected),
|
|
623
|
+
observedJson: JSON.stringify(input.observed),
|
|
624
|
+
screenshotPath: input.screenshotPath ?? null,
|
|
625
|
+
screenshotFormat,
|
|
626
|
+
consolePath: input.consolePath ?? null,
|
|
627
|
+
screenshotErrorJson:
|
|
628
|
+
input.screenshotError === null || input.screenshotError === undefined
|
|
629
|
+
? null
|
|
630
|
+
: JSON.stringify(input.screenshotError),
|
|
631
|
+
insecureJson:
|
|
632
|
+
input.insecure === null || input.insecure === undefined
|
|
633
|
+
? null
|
|
634
|
+
: JSON.stringify(input.insecure),
|
|
635
|
+
retained,
|
|
636
|
+
startedAt,
|
|
637
|
+
finishedAt,
|
|
638
|
+
};
|
|
639
|
+
if (hasCompiledPath) value.compiledPath = compiledPath;
|
|
640
|
+
let artifactIdForCleanup: string | null = null;
|
|
641
|
+
try {
|
|
642
|
+
// Validate BEFORE the INSERT, not after: `render_runs` has no unique
|
|
643
|
+
// schema-shape constraint SQLite would reject on its own (e.g. an
|
|
644
|
+
// empty `status`), so a post-insert `RenderRunSchema.parse` failure
|
|
645
|
+
// used to land in this same catch AFTER the row was already durable
|
|
646
|
+
// — unlinking evidence files a committed row still pointed at.
|
|
647
|
+
// Validating first means a schema-invalid input never reaches the
|
|
648
|
+
// INSERT, so the catch's cleanup-on-failure is only ever reached
|
|
649
|
+
// when no row was written.
|
|
650
|
+
const parsed = RenderRunSchema.parse(value);
|
|
651
|
+
const columns = [
|
|
652
|
+
"id",
|
|
653
|
+
"revision_id",
|
|
654
|
+
"tier",
|
|
655
|
+
"status",
|
|
656
|
+
"expected_json",
|
|
657
|
+
"observed_json",
|
|
658
|
+
"screenshot_path",
|
|
659
|
+
...(hasScreenshotFormat ? ["screenshot_format"] : []),
|
|
660
|
+
"console_path",
|
|
661
|
+
"screenshot_error_json",
|
|
662
|
+
"insecure_json",
|
|
663
|
+
"retained",
|
|
664
|
+
...(hasCompiledPath ? ["compiled_path"] : []),
|
|
665
|
+
"started_at",
|
|
666
|
+
"finished_at",
|
|
667
|
+
];
|
|
668
|
+
const values = [
|
|
669
|
+
value.id,
|
|
670
|
+
value.revisionId,
|
|
671
|
+
value.tier,
|
|
672
|
+
value.status,
|
|
673
|
+
value.expectedJson,
|
|
674
|
+
value.observedJson,
|
|
675
|
+
value.screenshotPath,
|
|
676
|
+
...(hasScreenshotFormat ? [value.screenshotFormat] : []),
|
|
677
|
+
value.consolePath,
|
|
678
|
+
value.screenshotErrorJson,
|
|
679
|
+
value.insecureJson,
|
|
680
|
+
value.retained ? 1 : 0,
|
|
681
|
+
...(hasCompiledPath ? [value.compiledPath ?? null] : []),
|
|
682
|
+
value.startedAt,
|
|
683
|
+
value.finishedAt,
|
|
684
|
+
];
|
|
685
|
+
this.db
|
|
686
|
+
.query(
|
|
687
|
+
`INSERT INTO render_runs(${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
|
|
688
|
+
)
|
|
689
|
+
.run(...values);
|
|
690
|
+
const ownerRow = this.db
|
|
691
|
+
.query("SELECT artifact_id FROM revisions WHERE id = ?")
|
|
692
|
+
.get(input.revisionId) as { artifact_id: string } | null;
|
|
693
|
+
if (ownerRow !== null) artifactIdForCleanup = ownerRow.artifact_id;
|
|
694
|
+
// Last-N retention runs AFTER the row is durable; a failure
|
|
695
|
+
// here would lose the just-recorded run, so it sits outside the
|
|
696
|
+
// INSERT transaction. Cleanup is best-effort — the row is
|
|
697
|
+
// authoritative, a stale on-disk file is recoverable.
|
|
698
|
+
if (artifactIdForCleanup !== null && this.options.evidenceRoot !== undefined) {
|
|
699
|
+
try {
|
|
700
|
+
enforceEvidenceRetention({
|
|
701
|
+
db: this.db,
|
|
702
|
+
artifactId: artifactIdForCleanup,
|
|
703
|
+
evidenceRoot: this.options.evidenceRoot,
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
// Retention cleanup is best-effort; do not fail the write.
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
hardenDatabaseFiles(this.db.filename);
|
|
710
|
+
return parsed;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
removeUnreferencedEvidence(this.db, [input.screenshotPath, input.consolePath, compiledPath]);
|
|
713
|
+
throw asStoreError(error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
promoteRevision(input: PromoteRevisionInput): Template {
|
|
718
|
+
return promoteLifecycleRevision(this.db, input);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
instantiateTemplate(input: TemplateInput): Template {
|
|
722
|
+
return createLifecycleTemplate(this.db, input);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
pinRevision(revisionId: string, pinned = true): void {
|
|
726
|
+
pinLifecycleRevision(this.db, revisionId, pinned);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
updateRevisionSource(_revisionId: string, _source: Uint8Array): never {
|
|
730
|
+
throw new FacetStoreError("immutable_revision", "Revision source and sha256 are immutable");
|
|
731
|
+
}
|
|
732
|
+
}
|