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:
- dashboard receipt: base
d286844, 319/319 tests, Portfolio, Project, Progress and Usage views checked in the Codex in-app browser, a 390 px mobile check with no horizontal overflow, and 0 console errors; - project guidance receipt: base
8ada02a, 350/350 tests, the values, actions and blocker pages served here. Their domain logic is accepted under Project guidance and Required actions and blockers.
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 seamcontract.ts, plusprotocol.ts,api.ts,router.tsx,format.ts,components.tsx,guidance-draft.ts,attention-list.tsx,brand.ts,App.tsx,main.tsxandviews/*Page.tsx); brand filesapps/dashboard/public/brand/(factory-mark.png,plumb-mascot.png) - Tests (46 in the root suite, plus 10 in the dashboard package):
dashboard-view(6) anddashboard-server(12);dashboard-guidance-attention(6) anddashboard-format(8);dashboard-editor(4) anddashboard-boundaries(5);dashboard-brand(5), dependency-free;apps/dashboard/test/brand-render(10), run by the dashboard package'snpm test(the second half of itscheck) and never by the root suite, because it imports the dashboard's ownreact,react-dom/serverandvite; the rootnpm run checknever 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.1and only answers its own loopback Host. Commands also need this exact Origin, aSec-Fetch-Sitethat is absent orsame-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) opensCommandJournal.openReadOnlyfor that request and closes it infinally; static files touch no journal. The two commands write only throughCommandJournal.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
Knownvalues, 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 domainCount, 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); frozenPERIOD_IDS = ["24h", "7d", "30d", "90d"]andTOKEN_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}, whereSourceRef = {collector, recordId, revision, digest}.- View DTOs:
PortfolioDto,ProductResponseDto,ProductDto/CorruptProductDto,TotalsDto,SourceStatusDtoandUsageGroupDto.ProductDto.operationScopeis"metadata-inspection"anddeliveryAuthorityis"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}andGuidanceSavedDto {…, replayed}. - Attention DTOs:
AttentionDto,AttentionItemDtoandBlockerDetailDto, whererecheckis always"unsupported". - Errors:
ApiErrorCodehas 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): GuidanceDtosumCounts(counts: readonly Count[]): Count(:179)SOURCES(:268) lists six descriptors:verification,work,pull-requests,releasesandusagearetrusted-import;outcomesisno-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">andPortfolioRead = Pick<PortfolioView, "registryVersion" | "usageVersion" | "registeredProducts" | "products" | "unallocatedUsage" | "usageSourceNotices">. - Internal: a corrupt Product's
detailis capped at 280 characters, andPROVIDER_NAMES = {anthropic: "Anthropic"}.
Server (src/dashboard-server.ts):
- Constants:
DASHBOARD_HOST = "127.0.0.1",DEFAULT_DASHBOARD_PORT = 4317, andDEFAULT_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: nullserves the API only, andport: 0picks a free port. Startup throws before listening on a badjournalPath(TypeError), a port outside 0–65 535 (RangeError), a journal that cannot be opened read-only (JournalNotFoundErroror another journal error) or a bundle beyond the static limits (Error); a failedlisten(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 throwsError. - Result: a frozen
DashboardServer {url, port, token, close(): Promise<void>}.
- Options:
USAGEand the command-line interface:node src/dashboard-server.ts --journal <path> [--port <n>] [--static <dir>] [--no-inspect] [--no-guidance-edits].--help/-hprintsUSAGEand 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);
maxHeaderSize16 384; default period7d; 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 perstartDashboardServercall.
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.
| Route | Returns / takes |
|---|---|
GET /api/session | SessionInfoDto; no query parameters |
GET /api/portfolio, /api/attention, /api/products/:id (?period=) | PortfolioDto, AttentionDto, ProductResponseDto |
GET /api/products/:id/guidance, …/guidance/revisions/:n | GuidanceResponseDto (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/:blockerId | BlockerDetailDto; the ID is URL-encoded |
POST /api/products/:id/inspect | Body 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}andreadResponse<T>(response).api.ts:api(session,portfolio,product,attention,blocker,guidance,guidanceHistorywithlimit=10,inspect,saveGuidance),useLoad<T>(key, load)andLoadable<T>.format.ts:UNKNOWN = "—",countText,reasonText,sourceText,validityText,availabilityText,branchText,relativeTime(instant, asOf)(measured against the view'sasOf, not the browser clock),attentionText,responsibilityText,countsTextandtargetPath.guidance-draft.ts:operationFor,failureOf(status, code): SaveFailureandproblemsOf.components.tsx:useRepositoryCheckruns inspections one after another;Navigationshows the mark beside the live-text wordmark;EmptyState({message, detail, calm})adds Plumb only whencalm.brand.ts:FACTORY_MARK(/brand/factory-mark.png, 72 px,minimumPx48),PLUMB_MASCOT(/brand/plumb-mascot.png, 128 px),EMPTY_PORTFOLIO(the brand guide's empty-Portfolio line) andcalmEmptyPortfolio(read, check).views/PortfolioPage.tsx:PortfolioView({period, state, reload})renders one read state;PortfolioPageloads and delegates to it.router.tsx:parseRoute,href,navigateandsetPeriod.
Invariants and guarantees
- Neutral contract.
dashboard-contract.tsimports nothing. The app compiles it withtypes: [](apps/dashboard/tsconfig.json). The browser leavesapps/dashboard/srconly throughcontract.ts, and its only packages arereactandreact-dom/client(boundaries tests 1 and 3). - Text stays text. The app never uses
dangerouslySetInnerHTML,innerHTML,outerHTML,insertAdjacentHTML,eval,new Functionordocument.write(boundaries test 2). - Pure projection.
dashboard-view.tsimports onlyattention.ts, the contract,portfolio.tsand the types fromproject-guidance.ts. It has no I/O and no clock:asOfis the server'snow()(boundaries test 4). - Explicit copies at the top level.
productDto(:78) selects eachProductViewfield by name, so a new top-level field is not served until it is chosen there;repository,focus, Counts, token maps, measurementwindowandsource, and sessionmodelspass through unchanged, so a field added inside those shapes is served without a projection change. A missing activity count throwsPortfolioError("CORRUPT"). - Unknown is never zero.
sumCountsreturnsexactonly when every part is known and no part carries a reason. Otherwise known parts with a positive total givepartialwith every reason kept, and a known/unknown mix addssome-projects-unknown. Nothing known, or a zero lower bound, givesunknown, and[]givesunknown ["no-projects"].CountValuerenders 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 shows0(view "sums…"; format "exact zero shows 0…"). - 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 everytrusted-importsource's portfolio standing, so those totals are at best lower bounds and no such source reads complete.totals.usage.undatedis a plain sum over readable groups with no marker,measuredValuestaysno-outcome-assessmentandno-contractsources staynot-measured. The other Products still render, and/api/products/:corruptis 500CORRUPT. - Value is never inferred.
totals.measuredValueis alwaysunknown ["no-outcome-assessment"], andoutcomesis alwaysnot-measured. - Source standing (
standing,:319), in order:completeif there is at least one Count and every Count is exact;problemon asource-unauthenticated,-unavailableor-corruptreason;noneif nothing is exact or partial and every reason isno-complete-coverage; otherwisepartial.no-contractalways givesnot-measured. A source with the same fact contract and an existing capability needs only aSOURCESchange (view test 5); a new fact shape or collection kind needs domain and contract changes too. - Every JSON response body carries
protocol, including errors and 404s.readResponserefuses a body without the matching protocol withINCOMPATIBLE. - Loopback only. The server binds
127.0.0.1, re-checks the bound address, and answers only Host127.0.0.1:<port>orlocalhost:<port>. Anything else gets 403 before any API or static handling. Absolute-form or//targets get 400, and there are no CORS headers. - 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
-waland-shmsidecars. 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…"). - API guard (
checkFetchSite). RefusesSec-Fetch-Siteother thansame-originornone, and anyOriginthat is not this origin. - Command guards (
commandBody,:488), in order after the API guard (12); a refusal records nothing. Capability flag →Originexactly this origin (absent is refused) →Sec-Fetch-Siteabsent orsame-origin→application/json→ timing-safeX-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)BUSYcheck → write. - Never creates a journal. Startup opens read-only and throws
JournalNotFoundErrorwhen the journal is missing. Writes useopenExisting. A journal removed between the scope check and the write gives 503JOURNAL_UNAVAILABLE, and no file is created (the DB-1 regression). - 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. - One inspection at a time per server instance.
inspectingis a flag in that instance's state; a concurrent inspection gets 409BUSYand is not queued. - 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 elseno-cache. With no build, app routes give 503 and the API still answers. - Inert responses. JSON escapes
<,>,&, U+2028 and U+2029. API responses carryCache-Control: no-store. Every response has the CSPdefault-src 'none'; script-src 'self'; …; frame-ancestors 'none', plusnosniff,DENY,no-referrer, COOP and CORPsame-originand 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'srootis the one deliberately served local path. - No borrowed facts, once the effect runs.
Appremounts the project, values and blocker pages by key, so a Product change starts empty. Within a page, a changeduseLoadkey (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. - One bad stream is one item. In
/api/attention, an unreadable Product or usage stream becomes onefacts-unreadableitem./api/portfolioinstead fails with status 500 when the shared usage stream is corrupt (guidance-attention "Actions stay readable…"). - Brand stays decorative. The mark (
alt="") sits beside the live-text wordmark. WhenregisteredProductsis 0 and no Product is listed, the Portfolio showsEMPTY_PORTFOLIOand a plain detail; Plumb (alt="") follows them only whencalmEmptyPortfolioholds: statusready(not data kept while reloading or after a failed reload), zero attention actions and notices, no source inproblem, and no repository check running or failed. Otherwise the same words appear without it. Both PNGs are byte-identical copies ofdocs/brand/assets/core, served from the static manifest asimage/pngwithno-cache(dashboard-brand; each read state rendered bybrand-render).
Failure semantics
| Status · code | Cause |
|---|---|
400 BAD_REQUEST | Bad 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 FORBIDDEN | A foreign Host, a cross-site or cross-origin request, a missing Origin or token, or a disabled capability |
404 NOT_FOUND | An 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_LARGE | Wrong method (with an Allow header) · not application/json · over the route's body cap, by Content-Length or streamed bytes |
409 STALE | The guidance revision moved on: "revision N is current. Nothing was saved." |
409 CONFLICT | CONFLICT or LIMIT from the domain, such as seven values or a reused commandId with other content |
409 BUSY · 409 UNAVAILABLE | An inspection is already running · PortfolioError("UNAVAILABLE") |
500 CORRUPT | Stored facts failed re-validation |
503 JOURNAL_UNAVAILABLE | The journal is missing, unsupported, busy, unreadable or refused the request (fixed text) |
503 UNAVAILABLE (text) · 500 INTERNAL | No bundle is built · anything else. If headers were already sent, the socket is destroyed. |
- Client-generated codes.
UNREACHABLE(status 0, fetch threw) andINCOMPATIBLE(protocol mismatch) are client-only.INTERNALis 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,BUSYorINCOMPATIBLE→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
commandIdand content replays withreplayed: trueand the originalrecordedAt, writing nothing; the same ID with other content is 409CONFLICT.operationForreuses 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 IDinspection/<capturedAt>. The same observation at the samecapturedAt(a fixed injectednow) is a duplicate and writes nothing; different content at that instant is 409CONFLICT. - Retries. The HTTP layer never retries a command on its own.
Portfolio.inspectProduct's import retries optimistic-concurrency conflicts up toMAX_IMPORT_ATTEMPTS(8) before 409CONFLICT. 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.
BUSYis 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-importorno-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);readAllreads 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;GitOptionsfromsrc/portfolio-inventory.ts. - Required actions and blockers (
src/attention.ts): the view usesattentionItems,attentionCounts,blockerDtoandAttentionOptions; the server usesprojectAttentionandprojectBlockerDetail. - 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_ENTRYandGuidanceError. - Command journal (
src/journal.ts):CommandJournal.openReadOnlyand.openExisting, plusJournalError,JournalNotFoundError,StorageErrorandUnsupportedSchemaError. - Node
http,crypto,fs,path,urlandutil. The browser uses React 19.3.0.
- Portfolio (
- Used by:
src/attention.tsimportsDASHBOARD_PROTOCOLand contract types.- No other
src/module imports the server or the view;npm run factorydoes not start the dashboard. It is started directly withnode src/dashboard-server.ts --journal <path>afternpm --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
enginesin bothpackage.jsonfiles;apps/dashboardhas 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 bothnpm run checkandnpm --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_PROTOCOLwhenever a served shape or closed code set changes incompatibly, including a newSourceCollectionkind.dashboard-view.test.tsasserts the literal string. A source over existing fact shapes with an existing capability needs only aSOURCESentry. - 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 forcomponents.tsx,styles.css,views/PortfolioPage.tsxanddocs/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
commandBodyandrequireRegisteredand writes only viaopenExisting; - 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 as0; - CSP stays
'self'(ViteassetsInlineLimit: 0); - brand illustrations stay out of tables, metrics, navigation (the identity mark is allowed), decisions and blocker repair, as the brand guide requires.
