Skip to Content
Architecture · Web application

Data for pages, shaped for the page.

What it is for

The pages of the web application do not query the read modelRead modelThe database tables used by the map, Ask and Find. NovusLens rebuilds these tables from its history of recorded changes. directly. The modules in apps/ui/lib do that work. They read one organisation’s records and check the signed-in viewer’s access. They prepare snapshots, overviews, briefs, findingsFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner., history lists and GuideGuideThe help pages inside NovusLens. They explain the pages, input formats and tasks in the version you use. They are product instructions, not evidence about the organisation. pages for display. A reader sees the result as the mapMapThe view of teams, services and their dependencies for the selected part of the organisation. Details appear beside the map., the overview and the briefBriefThe management brief: a printable summary of the differences to review, with their sources, observation dates and limits. It is a snapshot taken when it is printed.. Without this layer, each page would have to repeat the access rules and the wording.

The read model is the set of tables that the snapshot reads. This library reads those tables and records access and brief exports in query_audit, the access log.

One scoped snapshot feeds the overview, the overview feeds the brief, and a download records an export row.

How it works

Reading the read model

apps/ui/lib/twin.ts is the data-access module. It creates one pg connection pool at import time:

  • the connection string is DATABASE_URL when it is set;
  • otherwise it falls back to a local development address, 127.0.0.1 on port 5433, database conway, with a development user and password of the same name;
  • the pool gives up on a connection after 5000 milliseconds.

The module exports the functions that the pages call:

FunctionWhat it reads or does
getTwinSnapshot(viewer, org)Builds the full TwinSnapshot for one organisation, scoped to the viewer.
listTwins(viewer)Lists the organisations the viewer may see, with their counts and fidelityTwin fidelityCounts of the model's sourced facts at the told, shown and measured evidence levels. It describes the available support, not whether the claims are true. figures.
listGroups(viewer)Lists the groups of organisations the viewer may see.
getLoginRow(viewer)Returns the stored credential and identity for one viewer. Verification happens in session.ts, not here.
orgAccessState(viewer, org)Returns admitted, not-authorised or forgotten for one organisation.
recordBriefExport and briefExportStatusRecord that a brief was issued and check it later. See below.
ViewerRefusedErrorThe error thrown when a viewer may not read or export what was asked. See below.

The tables and views that twin.ts queries, by name, are:

  • actor_map, which maps each viewer to a role, an organisation and, for a business-unit owner, a scopeScopeThe work, data, activity or access that a rule, statement or calculation covers. For access, it names the records an account can see. For team measures, “Own operations” covers the selected unit. “Including descendants” adds the units beneath it. cellCellA box on the map that stands for a team, service, supplier or AI agent. It is a unit you can inspect, not necessarily a department or a person.;
  • query_audit, which every read writes to before it resolves;
  • cells, cell_car, attribute_provenance, evidence_artifacts, edges, cell_aliases, domains, cell_domains, org_meta, twin_fidelity, twin_cpl, twin_history;
  • findings, finding_aliases, artifact_requests, projection_runs, forgotten_orgs;
  • groups and group_members.

The SQLSQLStructured Query Language: the language used to read and change records in a database. in this library does not say which of the twin_* relations are views. The definitions live outside apps/ui/lib.

The snapshot is assembled in one function. It reads the latest finished projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. run from projection_runs. It then loads cells, scores, edges, critical path lengthCPLCritical path length. The total coordination weight of the longest chain of dependencies in the model. NovusLens reports it as index points, not elapsed time. (CPL), history, findings, requests, aliases, domains and alignment rows. The TwinSnapshot type names each of these. read_model is null when no projection run has finished, which the type comment says means the picture advances only on writes.

Current scores come from the reasoning service, not from the read model. getTwinSnapshot calls REASONING_URL/car/scores with a 15-second timeout. If that call fails, every axis of every cell carries the status unavailable with the reason “Current score evidence unavailable”. The declared values are kept separate. The same service provides the list of findings whose support is current, through REASONING_URL/findings/current-generated. If that call fails, the library uses a fallback list of tracked finding kinds and withholds every tracked finding, so none is shown as supported.

Scoping what a viewer sees

Every read takes a viewer and an organisation. The library resolves the viewer before it reads anything else.

resolveActor looks up the viewer’s rows in actor_map and keeps the first row that admits the organisation (the admission rule is orgAdmits in orgScope.ts). If none does, it throws ViewerRefusedError.

visibleCells then decides which cells the actor may see:

  • cxo, board and operator see every cell;
  • bu-owner sees the scope cell and its full subtreeSubtreeOne unit and every unit nested beneath it in the organisation's hierarchy. of descendants;
  • any other role gets the implementation layer (the infra cells) plus the domain cells that hold them.

Only the full view gets the organisation-wide lenses. CPL, fidelity, alignment and the business-domain view are returned only to cxo, board and operator. The library returns null or an empty list to other roles, because a subtree version of those numbers would be a different and misleading figure.

Scoping happens in the library, before a page renders.

Findings pass through present in findingScope.ts. An edge finding that touches one visible cell is shown in a partial form, worded for the side the viewer can see. A finding about a cell the viewer cannot see is hidden. Estate-wide kinds and org-wide findings are shown only in the full view. The rule reads config/finding-scope.json from the path two levels above the process working directory. The header comment says the same policy lives in packages/common_py/src/common_py/finding_scope.py.

History is scoped in the same way by historyScope.ts. The full view gets every change. A scoped reader gets only the changes about cells it may see, worded as partial dependenciesDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else. where needed. A scoped reader’s rows carry no totals.

The executive overview

overview.ts derives the executive overview from the snapshot. It does not query anything.

selectOverview builds candidate items in two ways:

  • For each cell on the observed critical pathCritical pathThe dependency chain with the greatest total coordination weight in the map. It identifies a structural constraint. It does not state elapsed time., it adds a dependency item. If that cell has no accountable ownerAccountable ownerThe person recorded as answerable for a team, service or agent. NovusLens does not assume that this person is also the one who runs it day to day., it also adds an ownership item.
  • For each finding that is not resolved or retracted, it adds one item. The finding’s category comes from its kind: a kind containing contradiction, contest, dispute or conflict gives contradiction. Regulation gives regulation. Owner, author, unowned capabilityCapabilityWhat a team can provide or an agent can do. NovusLens records the claim and the evidence that supports it. or delegation gives ownership. Everything else gives difference. A finding that names a cell missing from the snapshot is dropped, except an unowned-capability finding.

Candidates are sorted by stable ID within each category. The selection then takes one item from each category in turn, in the order dependency, ownership, difference, regulation, contradiction, before it takes a second round. It keeps the first five and reports the rest as omitted. The module’s own wording calls this a judgement about coverage and not a risk ranking.

Each item carries its basis, which is one of Measured observation, Calculated indicator or Interpretation. The basis comes from the finding kind, and any finding whose telemetryTelemetryMeasurements and activity records sent by a running system, such as ticket timings, network traffic or workflow results. is marked illustrative or simulated-estate is always Interpretation. Each item also carries its coverageCoverageWhat the available records cover, and what is missing. A count such as “3 of 5 dependencies measured” names the measured portion and the gap., an uncertainty note, the owner and one next step. issueLinks builds the links to the map and to the supporting records.

The management brief

brief.ts exports managementBrief. It renders one self-contained HTMLHTMLHypertext Markup Language: the format used to describe a web page's content and structure. document from the same overview selection. The document has up to five cards. Each names the issue, relevance, owner, next step, basis, source, observation date, coverage and uncertainty, with supporting links. It counts additional items and links to unresolved findings. A closing note explains that the dependency-chain index is a weighted structural score.

The download route, apps/ui/app/brief/download/route.ts, runs the steps in order: it reads the snapshot, renders the brief, and records the issue with recordBriefExport. That function runs in a REPEATABLE READ transactionTransactionA group of database changes that is saved together. If the group fails, its changes are not saved.. It checks that the viewer’s role has not changed. Every snapshot cell must still be visible to that viewer. It then writes a query_audit row with the prefix brief.export:. The row holds export ID, digestHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records. (sha256), evidence and score revisions, sorted cell IDs, role, and the note “generated for download”.

briefManifest.ts makes the evidence revision. It hashes a standardised JSONJSONJavaScript Object Notation: a text format for data, using named fields, lists and values. Configuration files and service messages use it. form of the whole snapshot with SHA-256SHA-256A method that turns content into a fixed-length fingerprint. Comparing fingerprints helps check whether content changed.. briefExportStatus reads the saved export row and takes a new snapshot under the original viewer’s scope. briefEvidenceStatus in briefManifest.ts compares the revisions. It returns unchanged or superseded, or unavailable when either revision is missing. The module comments say the revision supports the records and the calculation. It does not certify the brief’s prose. The check neither updates the file nor notifies recipients. NovusLens records issue of the brief without claiming that a recipient received it.

Findings, history and time

findings.ts groups findings that share kind, subject, status, owner, response and detail, for presentation only. It also decides which findings count as closed.

history.ts filters and describes the change rows in TimelineView and the changes list. It classifies each change as a finding, cell, score or edge change, and turns the stored before and after values into plain sentences.

format.ts is the one place that formats times and names. Reading times use the reader’s local time in day-month-year form to the minute. Exact times are shown in UTCUTCCoordinated Universal Time: a shared time reference that does not change with daylight-saving time. for details and exports. nameOf returns the label when one exists and the identifier otherwise.

agentEvidenceView.ts checks historical case queries. A cutoff must be a valid date in the past, and the query must use recorded policy mode.

hierarchy.ts decides where each cell is drawn. A nested domain cell that takes part in a dependency is drawn on the top-level flow map. A purely structural child is drawn inside its parent when the parent is opened. subtreeOf returns the full descendant tree and never visits the same cell twice.

The Guide

guide.ts reads the Guide pages from the guide_pages table. It applies no viewer check, because every role may read the Guide. The table is product-owned and has no organisation column. It is filled from config/guide.json by ./lens graph guide load (apps/graph/src/conway_graph/guide.py). That load replaces the whole set on each run, so a page the release no longer carries is removed. The ops script runs the load as its guide step.

config/guide.json itself is not read by the web application at run time. scripts/build_guide.py builds it from the authored MDX in apps/docs/content/guide/, and its --check mode refuses a file that has drifted from its sources. The contextual-help APIAPIApplication programming interface: a defined way for one program to request data or actions from another. calls guidePages(). If its database call fails, it returns an unavailable response. The full Guide uses the bundled static documentation and remains readable.

The web build runs scripts/docs-bundle.mjs before building the UI. It exports apps/docs under /docs, including a local Pagefind search index. The output is copied into apps/ui/documentation and into the Docker web image. The public documentation website keeps its root-path build.

documentation.ts serves this export through app/docs/[[...path]]/route.ts after the existing session check. It supports clean page paths, local JavaScript, images, diagrams, fonts and byte ranges for video seeking. It refuses paths outside the bundle and missing files. Files remain private to the signed-in reader. The documentation content does not require a database connection; the contextual help drawer still reads Guide records.

guideReferences.json retains generated citation-to-anchor mappings for the legacy reference documents. Manual URLs redirect to the complete local tutorials. Other reference documents remain served behind the same session.

Interfaces

ModuleWhat it exportsUsed by
twin.tsTwinSnapshot, CellRow, EdgeRow, FindingRow, HistoryRow, RequestRow, DomainRow, AlignmentRow, TwinListRow, getTwinSnapshot, listTwins, listGroups, getLoginRow, orgAccessState, recordBriefExport, briefExportStatus, ViewerRefusedError, defaultViewerapp/page.tsx (type), app/audit/page.tsx, app/brief/download/route.ts, app/brief/status/page.tsx, app/login/page.tsx, app/import/page.tsx, app/twins/page.tsx, components/AccountMenu.tsx, and types in many components
overview.tsselectOverview, selectionBasis, issueLinks, findingRows, findingHeadline, findingBasis, findingCategoryapp/overview/page.tsx, app/brief/page.tsx, app/evidence/page.tsx, components/FindingsList.tsx, components/TimelineView.tsx, components/rail/Rail.tsx, components/rail/NeedsDecision.tsx, brief.ts
brief.tsmanagementBriefapp/brief/download/route.ts
briefManifest.tsbriefEvidenceRevision, briefEvidenceStatustwin.ts
findingScope.tspresent, classify, visibility, partialSubject, partialDetail, anchorCellstwin.ts, historyScope.ts
historyScope.tsscopeHistory, scopeChangetwin.ts
hierarchy.tstopLevelCells, explodeChildren, subtreeOf, connectedIdscomponents/TwinGraph.tsx, components/TwinView.tsx, components/rail/RailPanels.tsx
findings.tsgroupFindingRecords, isClosedFinding, visibleFindingRecordscomponents/FindingsList.tsx
history.tsfilterHistory, historyKind, describeHistoryValuecomponents/TimelineView.tsx, components/rail/ChangesList.tsx
format.tsreadingTime, readingDate, exactTime, isoTime, ageWords, nameOfMost pages and components; brief.ts
agentEvidenceView.tshistoricalCutoff, caseQuery, matchesHistoricalSelectioncomponents/AgentEvidenceCases.tsx
guide.tsguidePages, GuidePageapp/api/guide/[id]/route.ts
documentation.tsserveDocumentationapp/docs/[[...path]]/route.ts
guideReferences.jsonCitation label to help-document anchorGenerated by scripts/guide_documents.py; checked by the Guide reference tests

Modules that call out, in addition to PostgresPostgresThe database software that stores the tables read by the NovusLens screens, along with document bodies, access records and protected personnel records., are twin.ts (the reasoning service endpointsEndpointA particular address on a service that accepts a defined kind of request. named above) and guide.ts (Postgres only). twin.ts takes its service address and headers from profile.ts, which is described on Session, profile and access.

Invariants and failure behaviour

Refusal. ViewerRefusedError is thrown with the viewer and the organisation. Its message is “viewer … is not known to org … — no default access”. Three functions throw it. getTwinSnapshot refuses a viewer not admitted to the organisation. recordBriefExport refuses changed roles or visible cells. briefExportStatus refuses an export under changed access. Each caller handles it differently:

  • The brief download route returns HTTPHTTPHypertext Transfer Protocol: the rules a browser or program uses to send a request to a web service and receive its response. 403 with the text “Access refused”.
  • The brief status page shows “This export is unavailable under your current access.”
  • The audit page wraps its navigation snapshot in a try block, so a refusal removes the navigation and leaves the page in place.

Other functions do not throw. listTwins and listGroups return empty lists for a viewer with no admitting row. orgAccessState returns not-authorised for an organisation the viewer is not admitted to. It discloses nothing about whether that organisation exists or was forgotten. It returns forgotten only to an admitted viewer.

Audit. Each snapshot read writes a query_audit row before the viewer is resolved. A refused attempt is recorded in the same way as a successful one. Listing twins and groups also writes a row.

Failure of the reasoning service. If the score call fails, each axis shows unavailable. If the list of current findings cannot be read, every tracked finding is withheld. Neither failure shows an unsupported figure as supported. A malformed export identifier throws an error with the message “Invalid export identity”. A missing export record returns null.

Failure of the database. A connection that cannot be made within 5 seconds raises an error from pg. The snapshot, export and login functions release their connection and let the error propagate. The contextual-help API catches it and returns an unavailable response. The bundled documentation does not use this database call.

Diagram

The download route, the access check, the audit row and the later status check.

Last updated