Factory docs, home
Page navigation

A local browser interface over the Portfolio, bound to loopback only. It has four parts: a neutral, versioned JSON contract; a pure projection of Portfolio, attention and guidance reads; an HTTP composition root with exactly two guarded commands; and a React presentation layer that chooses words, formats and lays out, and computes no domain Count.

Status: "Accepted local browser interface" (build review): "Neutral API contract, pure projections, HTTP composition and browser presentation; guarded repository refresh, desktop/mobile checks and five independent regressions." Receipts:

The dashboard receipt directly pins check output, screenshots and the review; that pinned review carries source hashes and pins a 28-file inventory, <local evidence file>. The guidance receipt pins a 35-file reviewed-source hash list, <local evidence file>. Both lists live under ignored <local evidence directory>, not Git, and identify the historical reviewed boundary, not the current source. This is accepted local infrastructure, not a Verdict, release or customer benefit, and all 27 epics remain open. Long-form: dashboard, human interface, design system.

Brand increment (0ab94ed–7b499ea; handover f5a8991): the handover records an Astra high PASS on round 2 with root 355/355 and dashboard package 10/10 tests. No receipt for it exists under docs/, and the build review has no row for it.

Verification: independent Astra reviews are archived at <local evidence archive>; the corrections from rounds 1–5 are applied here, and the highest-numbered report holds the current verdict.

  • Source: src/dashboard-contract.ts, src/dashboard-view.ts, src/dashboard-server.ts, apps/dashboard/src/ (the seam contract.ts, plus protocol.ts, api.ts, router.tsx, format.ts, components.tsx, guidance-draft.ts, attention-list.tsx, brand.ts, App.tsx, main.tsx and views/*Page.tsx); brand files apps/dashboard/public/brand/ (factory-mark.png, plumb-mascot.png)
  • Tests (46 in the root suite, plus 10 in the dashboard package):
    • dashboard-view (6) and dashboard-server (12);
    • dashboard-guidance-attention (6) and dashboard-format (8);
    • dashboard-editor (4) and dashboard-boundaries (5);
    • dashboard-brand (5), dependency-free;
    • apps/dashboard/test/brand-render (10), run by the dashboard package's npm test (the second half of its check) and never by the root suite, because it imports the dashboard's own react, react-dom/server and vite; the root npm run check never needs those packages.

Intelligence: none — Presents decided facts and counts (apps/dashboard renders them); a generated explanation would be optimistic copy over evidence.

What it hides

  • Transport and origin safety. The server binds 127.0.0.1 and only answers its own loopback Host. Commands also need this exact Origin, a Sec-Fetch-Site that is absent or same-origin, and a per-server-instance token, and every response the handler sends carries CSP and security headers. Static files come from a fixed manifest. The browser sees only typed DTOs.
  • Journal discipline. Every journal read (each GET or HEAD API route except /api/session) opens CommandJournal.openReadOnly for that request and closes it in finally; static files touch no journal. The two commands write only through CommandJournal.openExisting. The server never creates, migrates or seeds a journal. Journal errors become fixed text, because the originals name the path.
  • Domain interpretation for display. The projection selects each top-level DTO field explicitly (nested Known values, Counts, token maps, windows, source refs and session models pass through) and computes cross-project sums (exact, partial or unknown). It also decides each fact source's standing and collection capability. Measured value is always unknown. The browser computes no domain Count, standing or coverage; it formats them and only tallies what it shows (a page's attention items, resolved sessions, check results).
  • Versioning. Every JSON response body carries protocol. The browser refuses any other version instead of misreading it. Request bodies omit it: a command body must match its exact key set.

Public interface

Contract (src/dashboard-contract.ts, imports nothing, browser-safe):

  • Constants: DASHBOARD_PROTOCOL = "factory-dashboard/3" (:10); frozen PERIOD_IDS = ["24h", "7d", "30d", "90d"] and TOKEN_COUNTER_IDS = ["uncachedInputTokens", "cacheReadInputTokens", "cacheCreationInputTokens", "outputTokens"].
  • Count = {state: "exact", value} | {state: "partial", atLeast, reasons} | {state: "unknown", reasons}. Reasons are open codes.
  • Known<T> = {state: "unknown"} | {state: "known", value, source: SourceRef, observedAt, capturedAt, stale}, where SourceRef = {collector, recordId, revision, digest}.
  • View DTOs: PortfolioDto, ProductResponseDto, ProductDto / CorruptProductDto, TotalsDto, SourceStatusDto and UsageGroupDto.
    • ProductDto.operationScope is "metadata-inspection" and deliveryAuthority is "absent".
    • SourceCollection = "trusted-import" | "no-contract".
    • SourceState = "complete" | "partial" | "problem" | "none" | "not-measured".
  • Session and commands: SessionInfoDto {protocol, token, periods, defaultPeriod, capabilities: {inspect, guidance}}, InspectionDto, GuidanceDto, GuidanceSaveRequestDto {commandId, expectedRevision, values} and GuidanceSavedDto {…, replayed}.
  • Attention DTOs: AttentionDto, AttentionItemDto and BlockerDetailDto, where recheck is always "unsupported".
  • Errors: ApiErrorCode has 13 codes, listed under failure semantics (:492). ApiErrorDto = {protocol, error: {code, message}}.

Projection (src/dashboard-view.ts, pure):

  • projectPortfolio(read: PortfolioRead, meta: ViewMetaInput, sources: readonly SourceDescriptor[] = SOURCES): PortfolioDto (:52)
  • projectProduct(view: ProductView, meta: ViewMetaInput, sources = SOURCES, attention: AttentionOptions = NO_CHECKS): ProductResponseDto (:46)
  • guidanceDto(snapshot: GuidanceSnapshot): GuidanceDto
  • sumCounts(counts: readonly Count[]): Count (:179)
  • SOURCES (:268) lists six descriptors: verification, work, pull-requests, releases and usage are trusted-import; outcomes is no-contract.
  • Types: SourceDescriptor {id, name, collection, counts(facts): readonly Count[], factsBeyondCounts?(facts): boolean}, SourceFacts {product: Omit<ProductDto, "connections"> | null, usage: readonly UsageGroupDto[]}, ViewMetaInput = Omit<ViewMeta, "protocol"> and PortfolioRead = Pick<PortfolioView, "registryVersion" | "usageVersion" | "registeredProducts" | "products" | "unallocatedUsage" | "usageSourceNotices">.
  • Internal: a corrupt Product's detail is capped at 280 characters, and PROVIDER_NAMES = {anthropic: "Anthropic"}.

Server (src/dashboard-server.ts):

  • Constants: DASHBOARD_HOST = "127.0.0.1", DEFAULT_DASHBOARD_PORT = 4317, and DEFAULT_STATIC_DIR (apps/dashboard/dist, relative to the module).
  • DASHBOARD_LIMITS (:58, frozen): maxUrlLength: 2_048, maxCommandBodyBytes: 256, maxGuidanceBodyBytes: 24_576, maxStaticFiles: 256, maxStaticBytes: 8 * 1_048_576, maxStaticDepth: 4, maxMessageLength: 280, journalBusyTimeoutMs: 2_000.
  • startDashboardServer(options: DashboardServerOptions): Promise<DashboardServer> (:164)
    • Options: {journalPath: string; staticDir?: string | null; port?: number; allowInspect?: boolean; allowGuidance?: boolean; git?: GitOptions; now?: () => Date}. Both commands are enabled by default, staticDir: null serves the API only, and port: 0 picks a free port. Startup throws before listening on a bad journalPath (TypeError), a port outside 0–65 535 (RangeError), a journal that cannot be opened read-only (JournalNotFoundError or another journal error) or a bundle beyond the static limits (Error); a failed listen (a port in use, say) rejects with Node's own error; after listening it verifies the bound address, and a mismatch closes the server and throws Error.
    • Result: a frozen DashboardServer {url, port, token, close(): Promise<void>}.
  • USAGE and the command-line interface: node src/dashboard-server.ts --journal <path> [--port <n>] [--static <dir>] [--no-inspect] [--no-guidance-edits]. --help/-h prints USAGE and exits 0. Exit code 64 means bad arguments and 70 means startup failed. SIGINT or SIGTERM closes the server.
  • Internal values: timeouts of 10 000 ms (headers), 15 000 ms (request) and 5 000 ms (keep-alive); maxHeaderSize 16 384; default period 7d; Product ID /^[a-z0-9][a-z0-9-]{0,62}$/; blocker ID /^[A-Za-z0-9][A-Za-z0-9._:\/#@+-]{0,95}$/; a token of 32 random bytes in base64url, minted per startDashboardServer call.

HTTP routes. Every GET route also answers HEAD. ?period= is one of PERIOD_IDS (default 7d); a view's asOf is the server's now(), its window is [asOf − period, asOf) and staleAfterMs is PORTFOLIO_LIMITS.defaultStaleAfterMs. The period filters dated activity, verification, release and invocation Counts, and selects raw measurements whose own windows overlap the view window (kept with their own windows and values, never prorated or summed): registeredProducts, current repository state, open blockers and cumulative session totals ignore it, undated invocations are never allocated to a period, and source notices are those overlapping the window. Invocation and session figures are never added together, and session costEstimateUsd is a cumulative provider-reported estimate, not billing or period spend.

RouteReturns / takes
GET /api/sessionSessionInfoDto; no query parameters
GET /api/portfolio, /api/attention, /api/products/:id (?period=)PortfolioDto, AttentionDto, ProductResponseDto
GET /api/products/:id/guidance, …/guidance/revisions/:nGuidanceResponseDto (n is 1–6 digits)
GET /api/products/:id/guidance/history?before=&limit=GuidanceHistoryDto; before 1–501, limit 1–20
GET /api/products/:id/blockers/:blockerIdBlockerDetailDto; the ID is URL-encoded
POST /api/products/:id/inspectBody exactly {} (≤ 256 B) → InspectionDto via Portfolio.inspectProduct
POST /api/products/:id/guidance{commandId, expectedRevision, values} (≤ 24 576 B) → GuidanceSavedDto via ProductGuidance.setValues with DIRECT_ENTRY

These client routes return index.html: /, /actions, /progress, /usage, /projects/:id, /projects/:id/values and /projects/:id/blockers/:blockerId.

Browser seams (apps/dashboard/src; an app, not a library):

  • protocol.ts: class ApiError extends Error {status; code} and readResponse<T>(response).
  • api.ts: api (session, portfolio, product, attention, blocker, guidance, guidanceHistory with limit=10, inspect, saveGuidance), useLoad<T>(key, load) and Loadable<T>.
  • format.ts: UNKNOWN = "—", countText, reasonText, sourceText, validityText, availabilityText, branchText, relativeTime(instant, asOf) (measured against the view's asOf, not the browser clock), attentionText, responsibilityText, countsText and targetPath.
  • guidance-draft.ts: operationFor, failureOf(status, code): SaveFailure and problemsOf.
  • components.tsx: useRepositoryCheck runs inspections one after another; Navigation shows the mark beside the live-text wordmark; EmptyState({message, detail, calm}) adds Plumb only when calm.
  • brand.ts: FACTORY_MARK (/brand/factory-mark.png, 72 px, minimumPx 48), PLUMB_MASCOT (/brand/plumb-mascot.png, 128 px), EMPTY_PORTFOLIO (the brand guide's empty-Portfolio line) and calmEmptyPortfolio(read, check).
  • views/PortfolioPage.tsx: PortfolioView({period, state, reload}) renders one read state; PortfolioPage loads and delegates to it.
  • router.tsx: parseRoute, href, navigate and setPeriod.

Invariants and guarantees

  1. Neutral contract. dashboard-contract.ts imports nothing. The app compiles it with types: [] (apps/dashboard/tsconfig.json). The browser leaves apps/dashboard/src only through contract.ts, and its only packages are react and react-dom/client (boundaries tests 1 and 3).
  2. Text stays text. The app never uses dangerouslySetInnerHTML, innerHTML, outerHTML, insertAdjacentHTML, eval, new Function or document.write (boundaries test 2).
  3. Pure projection. dashboard-view.ts imports only attention.ts, the contract, portfolio.ts and the types from project-guidance.ts. It has no I/O and no clock: asOf is the server's now() (boundaries test 4).
  4. Explicit copies at the top level. productDto (:78) selects each ProductView field by name, so a new top-level field is not served until it is chosen there; repository, focus, Counts, token maps, measurement window and source, and session models pass through unchanged, so a field added inside those shapes is served without a projection change. A missing activity count throws PortfolioError("CORRUPT").
  5. Unknown is never zero. sumCounts returns exact only when every part is known and no part carries a reason. Otherwise known parts with a positive total give partial with every reason kept, and a known/unknown mix adds some-projects-unknown. Nothing known, or a zero lower bound, gives unknown, and [] gives unknown ["no-projects"]. CountValue renders unknown as — with its reason and partial as ≥ n; a wholly unknown row or group whose reasons all match (in the usage table, also with no undated records) collapses to "Not recorded · reason"; the Portfolio overview's verification summary says only "Not recorded", with the reason on the project page; only an exact Count shows 0 (view "sums…"; format "exact zero shows 0…").
  6. Corrupt Products are isolated. Each corrupt Product adds unknown ["corrupt-project"] to every aggregate Count (activity, verification, releases, dated usage and usage coverage) and to every trusted-import source's portfolio standing, so those totals are at best lower bounds and no such source reads complete. totals.usage.undated is a plain sum over readable groups with no marker, measuredValue stays no-outcome-assessment and no-contract sources stay not-measured. The other Products still render, and /api/products/:corrupt is 500 CORRUPT.
  7. Value is never inferred. totals.measuredValue is always unknown ["no-outcome-assessment"], and outcomes is always not-measured.
  8. Source standing (standing, :319), in order: complete if there is at least one Count and every Count is exact; problem on a source-unauthenticated, -unavailable or -corrupt reason; none if nothing is exact or partial and every reason is no-complete-coverage; otherwise partial. no-contract always gives not-measured. A source with the same fact contract and an existing capability needs only a SOURCES change (view test 5); a new fact shape or collection kind needs domain and contract changes too.
  9. Every JSON response body carries protocol, including errors and 404s. readResponse refuses a body without the matching protocol with INCOMPATIBLE.
  10. Loopback only. The server binds 127.0.0.1, re-checks the bound address, and answers only Host 127.0.0.1:<port> or localhost:<port>. Anything else gets 403 before any API or static handling. Absolute-form or // targets get 400, and there are no CORS headers.
  11. Reads never write. GET and HEAD open the journal read-only, make no schema or domain mutation and run no Git. Tests establish unchanged logical contents and a byte-identical main journal file; SQLite reader coordination may still create or update the -wal and -shm sidecars. A fake Git that leaves a marker proves the Git claim, and the inspect command is the positive control (server "reads never write…"; guidance-attention "every new GET and HEAD…").
  12. API guard (checkFetchSite). Refuses Sec-Fetch-Site other than same-origin or none, and any Origin that is not this origin.
  13. Command guards (commandBody, :488), in order after the API guard (12); a refusal records nothing. Capability flag → Origin exactly this origin (absent is refused) → Sec-Fetch-Site absent or same-origin → application/json → timing-safe X-Factory-Token → no query parameters → size cap on Content-Length and streamed bytes → JSON with the exact key set → valid Product ID → registered, checked read-only → (inspection only) BUSY check → write.
  14. Never creates a journal. Startup opens read-only and throws JournalNotFoundError when the journal is missing. Writes use openExisting. A journal removed between the scope check and the write gives 503 JOURNAL_UNAVAILABLE, and no file is created (the DB-1 regression).
  15. Two commands only. Only the repository check and a guidance edit exist, both for registered Products. No route registers Products, imports facts or marks a blocker resolved. Guidance provenance is always DIRECT_ENTRY, and a body that claims provenance is refused.
  16. One inspection at a time per server instance. inspecting is a flag in that instance's state; a concurrent inspection gets 409 BUSY and is not queued.
  17. Fixed static manifest (loadStatic, :708). Read once at startup: regular files only, no symlinks or dotfiles, names matching [A-Za-z0-9._-]+, extensions .html .js .css .svg .png .ico .woff2 .txt, within the count, byte and depth limits (beyond them, startup fails; a missing directory gives an empty manifest). A request path is only a manifest key; /assets/* is cached as immutable and everything else no-cache. With no build, app routes give 503 and the API still answers.
  18. Inert responses. JSON escapes <, >, &, U+2028 and U+2029. API responses carry Cache-Control: no-store. Every response has the CSP default-src 'none'; script-src 'self'; …; frame-ancestors 'none', plus nosniff, DENY, no-referrer, COOP and CORP same-origin and a Permissions-Policy. Responses carry none of the server's own journal path, device or inode identity or stack traces (journal errors become fixed text), and error messages are at most 280 characters. Source and user text (focus, blockers, guidance values) is served as supplied, escaped but not redacted, and a registered Product's root is the one deliberately served local path.
  19. No borrowed facts, once the effect runs. App remounts the project, values and blocker pages by key, so a Product change starts empty. Within a page, a changed useLoad key (a period change) clears the previous data only when its loading effect runs; the one render before it can still show the previous key's data under the new label. No test covers this.
  20. One bad stream is one item. In /api/attention, an unreadable Product or usage stream becomes one facts-unreadable item. /api/portfolio instead fails with status 500 when the shared usage stream is corrupt (guidance-attention "Actions stay readable…").
  21. Brand stays decorative. The mark (alt="") sits beside the live-text wordmark. When registeredProducts is 0 and no Product is listed, the Portfolio shows EMPTY_PORTFOLIO and a plain detail; Plumb (alt="") follows them only when calmEmptyPortfolio holds: status ready (not data kept while reloading or after a failed reload), zero attention actions and notices, no source in problem, and no repository check running or failed. Otherwise the same words appear without it. Both PNGs are byte-identical copies of docs/brand/assets/core, served from the static manifest as image/png with no-cache (dashboard-brand; each read state rendered by brand-render).

Failure semantics

Status · codeCause
400 BAD_REQUESTBad target, an unknown or repeated query parameter, a bad period or integer, non-JSON or a wrong body shape, GuidanceError/PortfolioError INVALID, or an unreadable body. An over-long URL is 414 with this code.
403 FORBIDDENA foreign Host, a cross-site or cross-origin request, a missing Origin or token, or a disabled capability
404 NOT_FOUNDAn unknown route, an invalid or unregistered Product ID, or an unknown blocker or revision
405 METHOD_NOT_ALLOWED · 415 UNSUPPORTED_MEDIA_TYPE · 413 PAYLOAD_TOO_LARGEWrong method (with an Allow header) · not application/json · over the route's body cap, by Content-Length or streamed bytes
409 STALEThe guidance revision moved on: "revision N is current. Nothing was saved."
409 CONFLICTCONFLICT or LIMIT from the domain, such as seven values or a reused commandId with other content
409 BUSY · 409 UNAVAILABLEAn inspection is already running · PortfolioError("UNAVAILABLE")
500 CORRUPTStored facts failed re-validation
503 JOURNAL_UNAVAILABLEThe journal is missing, unsupported, busy, unreadable or refused the request (fixed text)
503 UNAVAILABLE (text) · 500 INTERNALNo bundle is built · anything else. If headers were already sent, the socket is destroyed.
  • Client-generated codes. UNREACHABLE (status 0, fetch threw) and INCOMPATIBLE (protocol mismatch) are client-only. INTERNAL is shared with the server (500 above); the client also generates it for a non-JSON reply or an unexpected client error.
  • failureOf. STALE → stale (nothing written; the draft is kept). Status 0, status ≥ 500, BUSY or INCOMPATIBLE → uncertain (the write may have happened; retry the same operation). Anything else → refused (the server answered and wrote nothing).
  • Idempotency. A guidance save with the same commandId and content replays with replayed: true and the original recordedAt, writing nothing; the same ID with other content is 409 CONFLICT. operationFor reuses the ID only while revision and content are unchanged. Inspection has no caller-supplied idempotency key: each success normally records a new repository fact with record ID inspection/<capturedAt>. The same observation at the same capturedAt (a fixed injected now) is a duplicate and writes nothing; different content at that instant is 409 CONFLICT.
  • Retries. The HTTP layer never retries a command on its own. Portfolio.inspectProduct's import retries optimistic-concurrency conflicts up to MAX_IMPORT_ATTEMPTS (8) before 409 CONFLICT. The repository check reports availability (missing, replaced, not-repository, not-primary, unreadable) as an observation, not an error.

Trust scope

Established locally (tests and receipts):

  • The loopback, Host, Origin and token guards against browser pages from other origins, including DNS rebinding.
  • Git-free reads that leave the journal's logical contents and main file unchanged (SQLite sidecars aside).
  • No journal creation, including the removal race.
  • Honest exact, partial and unknown rendering.
  • Isolation of a corrupt Product or stream.
  • Guarded guidance saves, stale handling and replay across a server restart.
  • One real "Check again" that moved the captured HEAD.
  • Desktop and 390 px mobile layouts with no horizontal overflow and no console errors, in the Codex in-app browser.

Not established:

  • Local callers. Protection from other local software or users. Any local process with a valid Host can read /api/session (which serves the token) and send commands, and there is no TLS.
  • Shared exclusion. BUSY is a flag in one server instance's state, so separate instances, even in the same process, can inspect at once.
  • Collection. Nothing collects automatically: every source is trusted-import or no-contract, and the dashboard cannot connect a source.
  • Blockers. No blocker recheck exists, and a "resolved" blocker is only as the source reported it, never verified here.
  • Authority and value. There is no Outcome Assessment, planner or delivery authority.
  • Browsers and scale.
    • Cross-browser support and a formal accessibility audit.
    • Behaviour beyond PORTFOLIO_LIMITS.maxProducts (100); readAll reads at most two pages of 100.
  • Beyond local. Any release, production or customer claim.

Composition

  • Depends on:
    • Portfolio (src/portfolio.ts): Portfolio (listProducts, portfolioView, productView, currentConditions, blockerRecord, inspectProduct), PORTFOLIO_LIMITS.maxPageSize (100) and .defaultStaleAfterMs (86 400 000), PortfolioError; GitOptions from src/portfolio-inventory.ts.
    • Required actions and blockers (src/attention.ts): the view uses attentionItems, attentionCounts, blockerDto and AttentionOptions; the server uses projectAttention and projectBlockerDetail.
    • Project guidance (src/project-guidance.ts): ProductGuidance (current, history, revision, setValues), GUIDANCE_LIMITS (6 values, 280 characters, 16 384 B, 500 revisions, pages of 20), DIRECT_ENTRY and GuidanceError.
    • Command journal (src/journal.ts): CommandJournal.openReadOnly and .openExisting, plus JournalError, JournalNotFoundError, StorageError and UnsupportedSchemaError.
    • Node http, crypto, fs, path, url and util. The browser uses React 19.3.0.
  • Used by:
    • src/attention.ts imports DASHBOARD_PROTOCOL and contract types.
    • No other src/ module imports the server or the view; npm run factory does not start the dashboard. It is started directly with node src/dashboard-server.ts --journal <path> after npm --prefix apps/dashboard run build.
  • Not Repository integration. The repository check is Portfolio.inspectProduct, which inspects existing checkouts. It is not Repository sealing / export and integration.

Changing it safely

  • Run (Node ≥ 26.8.1, per engines in both package.json files; apps/dashboard has its own manifest and lockfile pinning React 19.3.0, TypeScript 7.0.2 and Vite 8.3.1): node --test test/dashboard-*.test.ts test/attention.test.ts (focused), npm --prefix apps/dashboard run build (browser typecheck and bundle), and before acceptance both npm run check and npm --prefix apps/dashboard run check (build, then the package's rendered tests). Layout and behaviour also need a real-browser check at desktop and 390 px widths, recorded in a new receipt.
  • Which tests prove what:
    • view: standing, period scope, corrupt Products, replaceable descriptors and sums;
    • server: loopback and Host, periods, read-only reads, command guards, BUSY, the static manifest, escaping, missing or corrupt stores and DB-1;
    • guidance-attention: guidance commands, restart and replay, attention and blocker routes, no resolve route, read-only GET and HEAD;
    • format and editor: words, lower bounds, protocol refusal, operation reuse and failure classes;
    • boundaries: the import graph and no HTML injection;
    • brand: byte-identical copies and provenance, mark size, Plumb placement and copy, PNG serving (root); the real Portfolio view rendered for each read state (package).
  • Contract changes. Bump DASHBOARD_PROTOCOL whenever a served shape or closed code set changes incompatibly, including a new SourceCollection kind. dashboard-view.test.ts asserts the literal string. A source over existing fact shapes with an existing capability needs only a SOURCES entry.
  • Receipts. Editing a listed file makes its bytes differ from the guidance review's retained hash; unlisted or new files (brand.ts, for one) are outside that 35-file inventory, which predates the brand increment and already differs for components.tsx, styles.css, views/PortfolioPage.tsx and docs/dashboard.md. A behaviour change needs a fresh independent review and a new receipt, plus updates to dashboard and the build review row. Never edit an existing receipt.
  • Reviewers check:
    • no GET or HEAD path writes or runs Git;
    • any new command goes through commandBody and requireRegistered and writes only via openExisting;
    • no journal path, device identity or raw provider data reaches a response;
    • the contract stays import-free;
    • the browser still computes no domain Count, standing or coverage, and never shows unknown as 0;
    • CSP stays 'self' (Vite assetsInlineLimit: 0);
    • brand illustrations stay out of tables, metrics, navigation (the identity mark is allowed), decisions and blocker repair, as the brand guide requires.

Source: docs/agents/dashboard.md