Skip to Content
Architecture · Read modelRead modelThe database tables used by the map, Ask and Find. NovusLens rebuilds these tables from its history of recorded changes.

The measurement engine, which reports only what it can source.

The refresh runs three stages in order, each writing its own table. The two analysers run only on request.

What it is for

The engine turns the read model into the figures and findingsFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner. that the screens show. Outputs include 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), evidence counts, change history, and Complexity, Agility and RecoverabilityRecoverabilityThe R in C/A/R: how well a team or service restores acceptable service after a failure. Recovery times and tested alternatives can supply evidence. (C/A/RC/A/RThree separate scores for a team or service: Complexity, Agility and Recoverability. Each uses current evidence and the customer’s stated scoring rules.) scores. CPL is the greatest total coordination weight along a dependencyDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else. chain between domain cellsCellA 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.. It also raises the structural findings (key-person exposure, boundary misalignment, cross-region couplingCouplingA link where one team or service depends on another. Its Loose, Buffered or Rigid class describes the time needed for a handoff. in a group) and keeps the evidence that backs them.

How it works

The engine is a set of functions in conway_graph.engine. The reasoning service’s refresh (apps/reasoning/src/conway_reasoning/operations.py) runs them after each projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. pass, in this order: compute_cpl for the asserted layer and then the observed layer, compute_fidelity, and record_history. Each stage runs on its own. A failure is recorded against that stage and the next stage still runs. The command ./lens graph engine --org ORG runs the same steps by hand.

Critical path length

Both layers share the filter, the weighting and the longest-path step. Only the observed layer can end with no figure from a lack of live measurement.

compute_cpl(conn, org, layer, now_ms=None) works on two layers.

  • The asserted layer is the interview picture. Every asserted edge between two domain cells counts.
  • The observed layer is the corrected picture. A measured edge replaces the stated handoffHandoffWork or information passed from one team or service to another. Its measured or stated completion time determines the dependency class. for the same consumer, provider and capabilityCapabilityWhat a team can provide or an agent can do. NovusLens records the claim and the evidence that supports it.. Its valid_until_ms must be empty or in the future. An expired observation stays in the table, is counted as expired_observations, and is ignored. An edge with no measurement keeps its asserted handoff.

Some rules apply to both layers.

  • Both endpointsEndpointA particular address on a service that accepts a defined kind of request. of an edge must be domain cells. Infrastructure-to-infrastructure edges do not enter the headline figure.
  • An edge whose handoff is Unknown is removed before the arithmetic. The engine stores no count of these. The screen counts them from the edge rows.
  • Where several edges join the same pair of cells, the heaviest one sets the weight. The pair counts as measured only when that winning edge is measured.
  • Each handoff contributes a coordination weight from OVERHEAD_HOURS in conway_mesh.model. The longest path is found by longest_path in common_py.cpl.
  • If the observed layer has no live measured row, the engine deletes that layer’s row in twin_cpl and returns nothing. A stale figure is never kept.
  • If the graphGraphA model made of units and links between them. In NovusLens it records teams, services and their dependencies. holds cyclesDependency cycleA loop in the dependencies: one unit needs another, which eventually needs the first unit again. For example, Sales waits on Finance and Finance waits on Sales. that exceed the path-search budget (CplBudgetExceeded), no figure is written. The engine opens a cpl-not-computable finding for the layer, listing the cyclic cells, and deletes the layer’s twin_cpl row.

The result is stored in twin_cpl, one row per organisation and layer. It holds cpl_hours, ordered cell identifiers in critical_path, path couplings in path_edges, expired_observations and weights_version. For the observed layer, observed_edges counts measured path couplings. The phrase “corrected, N of M measured” is read from observed_edges and path_edges.

Evidence counts (twin fidelity)

compute_fidelity(conn, org) counts every sourced fact the twinTwinNovusLens’s model of one organisation, built from records about its teams, services and how they rely on one another. It is only as current as those records. holds, and does so live.

  • Each attribute with provenanceProvenanceThe record of where a value came from, who stated it and when. Every value keeps this record. in attribute_provenance counts at its own rungRungOne of three levels of support for a recorded value: told, shown or measured. The level appears beside the value..
  • Each asserted dependency counts at its own rung. It counts as measured only when the live observed row for the same dependency is itself at the measured rung. An observed row at a lower rung is not progress.

The database stores the rungs as asserted, documented and measured. The engine reports them as told, shown and measured.

A score slot that is blank in both layers is excluded from the count and reported on its own. Infrastructure cells are reported separately, so that blanks by design are not read as gaps. The result is written to twin_fidelity and returned as told, shown, measured, sourced, excluded and excluded_infra.

History

record_history(conn, org) appends a twin_history row only when the picture has changed. The snapshot holds the four fidelity countsTwin 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., the asserted and observed CPL, and the number of open findings. The engine also records each cell’s accountability record, each layer’s scores, every finding and every edge, and compares that state with the last stored one. An unchanged picture stores nothing.

Each row keeps a changes list. Every record is listed as recorded, updated or no longer present, with its before and after values. The function returns True when it writes a row and False otherwise, including when fidelity has never been computed. The 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. keys of an edge or finding (provider, capability, kind and scope) are excluded from the comparison, so adding them is not a change. They are kept on each change so that a reader’s view of the history can follow the same visibility rules as the findings.

C/A/R component assessment

The module conway_graph.car_assessment carries VERSION = "car-boundaries-v2". Its replay function also accepts the earlier car-boundaries-v1. The three axes are complexity, agility and recoverability. Seven metrics feed them. Complexity draws on systems and coordinated changes. Agility draws on changes, failed changes and lead hours. Recoverability draws on restoration hours and recovery tests. Hours are reported as totals, never as means or percentiles.

The assessment follows these rules.

  • It assesses structural components only. It works from a set of evidence records about cells that the caller has already filtered by the sealed identity joinSealed identity joinStored links between employee fingerprints and teams or services. The fingerprints replace readable employee identifiers. Product pages do not show the links for individual people. and the visibility rules. It takes no person identifiers and performs no financial aggregation, and it infers no probability.
  • A parent score is never an average of child scores. A parent scope is recomputed from its own evidence and dependency structure, for either own or subtree scope.
  • Each axis reads from one of two sources. The first is an explicit calibrationCalibrationThe customer's own rule that turns raw figures, such as a gauge, a workflow history or an incident log, into a C/A/R score. A score moves when the calibration moves, and the change is recorded., a shown-rung formula over an exact inventory of populations. The second, for own-operation scope or a leaf, is a connectorConnectorSoftware that reads records from a source system, such as an incident tool. A customer-written file connects those records to teams or services in NovusLens. axis observation at the measured rung.

The main functions are these.

  • validate(bundle) refuses unknown fields, personal records and loops in the ownership tree. Every evidence row needs a source, an evidence rung and a validity window longer than zero.
  • assess(bundle, root, scope=…, now=…) returns the members, the components, the structure, the coverage, the findings and the input sources, with a snapshot digestHashA fixed-length fingerprint calculated from content. Comparing fingerprints helps detect changes and match records.. Its headline_scores field is None. Duplicate records with the same identifier are ignored. Conflicting versions are excluded and reported as conflicting-evidence. Overlapping populations are excluded and the total is marked partial. The search for all units required through mandatory dependencies is capped at 200,000 steps.
  • scores(bundle, assessment, axis_observations) returns, for each axis, a value with status available, conflict or unknown. A conflict is never resolved by picking a source. Values are clamped between 0 and 100. A calibration is withheld when required evidence is missing, conflicting, declared only (told), expired, or changed in scope.
  • affected_scopes(before, after) is deliberately conservative. Any change to the bundle invalidates every non-external cell.
  • replay(record) recomputes an archived assessment at its recorded time. It restores the axis scores stored with that record, and it restores the agent-evidence eligibility the record holds.

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. is reported as cell presence only, and complete is always False. The assessment never claims that a required population is complete.

Key-person risk

keyperson.analyse(conn, tw, org, singularity=4) reads 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. from twin_cpl. For each distinct cell on it, the analyser calls the database function critical_path_owner_count (migrationMigrationAn ordered change to a database's tables or rules. Applied migration files are recorded so the same file is not run again. 0021). It counts distinct person hashes for the cell and its nested units. Only sealed identity rows within their retention period count. It returns an integer and never an identity.

A count from 1 to 4 raises a key-person-risk finding with the identifier key-person-{org}-{cell}. A count of None (no sealed data) or 0 (no mapped personnel) raises nothing here. Zero mapped personnel does not establish missing ownership. The argument singularity is capped at 4, one below the privacy floorPrivacy floorThe rule that a unit with fewer than five mapped people is shown as a small team, never as a number. The key-person finding says fewer than five and never says who. of 5. A unit on the measured path with one to four mapped people is labelled a small team. The exact count is withheld to protect their identities. The finding records mapped_person_count as null, below_privacy_floor as true and the floor as 5. Its text labels the path measured and the personnel mapping documented. It does not describe observed work. The owner decides what happens next.

The analyser is called from the meshMeshA set of connected Mycelium services that exchange updates and keep the NovusLens log. seed walk (conway_mesh.seed_walk) and by its own command, ./lens graph keyperson --org ORG. The projection refresh does not call it.

Boundary misalignment

misalignment.analyse(conn, tw, org) compares two views of the organisation: the ownership tree of cells, and the declared business domainsDomainA business area defined by the work it does, such as purchasing or customs. It may involve several teams, and one team may serve several areas. with the cells that serve them. A “team” is the division level below a single organisation root. If the organisation has several roots, a team is the top cell of each tree. Before counting findings, the analyser excludes teams nested under other teams in the same set. A parent and child therefore do not count as separate teams. The analyser raises three kinds of finding.

  • boundary-smeared-domain: a domain is served by more than one team.
  • boundary-overloaded-team: a cell serves more than one domain.
  • boundary-unowned-domain: a domain is declared and no cell serves it.

Each is evidence of where team boundaries and business boundaries diverge. The Domains view reads these kinds. No code outside the test suite in this repository calls analyse. These findings are produced when its own command, ./lens exec -- python -m conway_graph.misalignment --org ORG, is run.

Groups

conway_graph.group holds a group: a set of regional twins viewed together. group_snapshot builds the consolidated view from aggregate rows for each member. Those rows are the asserted and observed CPL, the told, shown and measured counts, the domain count and the open-findings count. The snapshot also includes the verified federation factsFederationSharing selected signed facts and summaries between separate regional twins, while their detailed records stay in their own regions. for cross-region dependencies. It never reads another region’s cells, edges or attributes.

ingest_fact checks each federation fact with federation.verify_trusted. A fact that fails is refused with FactRejected and is not stored. A verified fact is stored with the handoff Unknown, because cross-region coupling is unknown until it is measured. The snapshot therefore states that the group-wide path is partial. The reasoning service’s route admits a fact only for the operator or for a member organisation’s cxo role. That membership check sits in the route, not in ingest_fact.

may_read_group lets the operator read any group. Any other actor reads a group only when their organisation is a member, and list_groups applies the same scope.

Actors

Viewer accounts have one of five roles: cxo, bu-owner, infra, operator or board. hash_passphrase stores a passphrase as PBKDF2-HMAC-SHA256PBKDF2-HMAC-SHA256The password-storage method used here. It repeatedly mixes the passphrase with extra random data to make guessing stored passphrases more expensive. with 120,000 iterations, a random 16-byte saltSaltExtra data mixed into a value before its fingerprint is calculated. It makes matching or guessing the original value harder. and a 32-byte derived key. The stored form is pbkdf2_sha256$iterations$salt$hash. A plain passphrase is never stored.

upsert_viewer replaces the role, scope cell and organisation on each call. It replaces the stored hash only when a new passphrase is supplied, so an omitted passphrase leaves the credential unchanged. Inside the containerContainerA running program with its own packaged software and isolated environment, managed by Docker on the host. it is python -m conway_graph viewer; on the host, ./lens graph viewer runs create-viewer instead. The operator commands ./lens graph create-viewer and ./lens graph update-viewer call create_viewer and update_viewer, which refuse an existing or a missing account respectively. This module only hashes and stores. The login check is elsewhere.

Stored evidence documents

conway_graph.evidence stores the bodies of solicited documents, called artifactsArtifactA stored source file or evidence body that a record can cite. Keeping the exact content lets a reader check what supported the record., in PostgresPostgresThe database software that stores the tables read by the NovusLens screens, along with document bodies, access records and protected personnel records.. Each one is keyed by its organisation and the SHA-256SHA-256A method that turns content into a fixed-length fingerprint. Comparing fingerprints helps check whether content changed. digest of its bytes. Storing the same bytes again for the same organisation returns the existing identifier. After purgePurgeRemoving a stored document’s contents while keeping a record that it was removed and why. Conclusions that used it must be checked again., the same bytes get a new identifier with a suffix such as .r2. A note links them to the purged copy. The purged row keeps its tombstoneTombstoneA retained record that says content was removed, when it was removed and why. The removed content itself is no longer available., and nothing it supported returns.

store_artifact commits its own transactionTransactionA group of database changes that is saved together. If the group fails, its changes are not saved.. store_artifact_in_transaction leaves the commit to the caller, so that durable intake can insert the artifact and its outboxOutboxA database list of saved messages waiting to be published. A message stays on the list until its progress is recorded. entry together. Boundary acquisition uses the second form.

resolve_citation(conn, artifact_id, org=None) returns the kind, filename, body and solicited-by note of an artifact. It raises LookupError when no artifact exists or the body was purged. It raises ValueError when the stored bytes no longer match the digest in the identifier.

Boundary support

conway_graph.boundary_support decides whether a boundary finding is supported by current evidenceCurrent evidenceA view that uses only measurements recent enough to count. The “What we were told” view uses recorded statements instead.. The finding kinds are control-gap-beyond-authority, control-gap-declaration-missing and control-gap-declared-control-unmet. Acquisition keeps one head per source. A newer acquisition replaces the head. Two acquisitions at the same moment for different artifacts mark the source as conflicted. An older acquisition changes nothing.

supported returns False unless every one of these holds.

  • The support object has exactly the expected fields, version 1 and the boundary-comparison rule.
  • The cell matches the finding and is visible to the reader.
  • The head for the source names the same artifact, and the source is not conflicted.
  • The artifact resolves and its body names this cell.
  • The freshness window is valid. The refresh interval lies between 1 and 300,000 milliseconds. The support expires at the written time plus three refresh intervals. The written time is not later than the current time plus the clock-skew allowance, which defaults to 5,000 milliseconds.
  • The cell is an agent or an external party.
  • The cell’s structure is unchanged: its label, its kind and its asserted capabilities, and for an agent also its authorityAuthorityWhat a team or agent is allowed to decide or do. For an agent, this comes from a named person or system that grants permission. and owner.

Interfaces

Entry pointReadsWritesCalled from
engine.compute_cpledges, cells, twin_cpltwin_cpl, and a cpl-not-computable finding when a cyclic search exceeds its budgetthe reasoning refresh, the engine command, the seed walk
engine.compute_fidelityattribute_provenance, edges, cell_car, cellstwin_fidelitythe reasoning refresh, the engine command
engine.record_historytwin_fidelity, twin_cpl, findings, cells, cell_car, edgestwin_historythe reasoning refresh, the engine command
car_assessment.validate, assess, scores, replaya bundle passed in; no databasenothingthe reasoning car module, which imports it; ./lens exec -- python -m conway_graph.car_assessment
keyperson.analysetwin_cpl, cells, critical_path_owner_countkey-person findingsthe seed walk; ./lens graph keyperson
misalignment.analysedomains, cell_domains, cellsboundary findings./lens exec -- python -m conway_graph.misalignment
group.group_snapshot, may_read_group, list_groupsgroups, group members, twin_cpl, twin_fidelity, domains, findings, federation factsnothingthe reasoning service’s group routes
group.ingest_factthe signed factfederation factsthe reasoning service’s route; the seed walk
actors.create_viewer, update_viewer, upsert_vieweractor mapactor map./lens graph create-viewer, ./lens graph update-viewer; the upsertUpsertAdd a record if its identifier is new, or update the record with that identifier if it already exists. only as ./lens exec -- python -m conway_graph viewer
evidence.store_artifact, store_artifact_in_transactionevidence artifactsevidence artifactsthe connectors, including boundary acquisition
evidence.resolve_citationevidence artifactsnothingthe reasoning tools and the evidence view
boundary_support.supportedboundary heads, evidence artifacts, cells, edgesnothingthe reasoning finding support

The projection and the rebuild rules that feed these tables are on the projection page. The tables themselves are listed on the schema page.

Invariants and failure behaviour

  • Every figure is recomputed from stored rows. None is typed by hand.
  • An unknown coupling carries no weight and is excluded, never defaulted.
  • An expired observation is excluded from the current picture and counted. It is not erased.
  • When the corrected layer has no live measurement, the stale figure is removed. A cyclic graph produces a finding and no number only when the exact path search exceeds its work budget.
  • A fact counts as measured only when its own row is at the measured rung.
  • The history stores change points only. An unchanged picture adds nothing.
  • The key-person finding carries neither a name nor a count. Every finding it raises is for a unit below the privacy floor of 5.
  • A C/A/R score is never averaged from child scores. Conflicting claims, missing populations, told-only evidence, expired calibrations and changed scope each withhold the number and say why. A partial total says that it is partial.
  • A federation fact is verified before it is stored. The current keys are derived from the region name, so the check confirms the canonical formCanonical JSONA fixed way to write data: sort the field names and omit spaces between fields. Do not use infinity or invalid numbers. Sender and receiver can then sign and check identical content. of a fact but not who signed it. Every fact carries that statement in its verification field.
  • Evidence is stored under its content digest. A citation either resolves to the exact bytes or fails with an error.
  • A refresh stage that raises an error is recorded against that stage, and the remaining stages still run.

The findings above are evidence for people to resolve. The engine does not decide a question for them, and it does not recommend a re-organisation or a change to any person’s role.

Last updated