Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Standpoints and Bitemporal Reasoning

A real-world domain has more than one viewpoint, and a real-world fact has more than one timestamp. A Lease looks different to the tenant than to the landlord. A regulation that takes effect on January 1 was added to the system on December 14. A medical record that names a diagnosis on Tuesday was retracted on Thursday because of new lab results — but the patient was treated under the original diagnosis for two days.

Argon makes these dimensions first-class. Standpoints are named perspectives the runtime tracks alongside every fact. Bitemporal axes — valid-time and transaction-time — are recorded for every event in the append-only log. Forks let you branch the world structurally without copying data. This chapter covers all three.

The mechanism: standpoints are a project-level partial-order lattice; modules can declare themselves contributing to a particular standpoint; rules in a child standpoint can override or extend rules from a parent. Bitemporal axes record both when a fact is true in the world and when the system learned it. Forks branch the event log structurally with copy-on-write semantics. Multi-standpoint bridge rules let a child standpoint pull facts from incomparable siblings via a shared parent. Refinement-type membership under open-world assumption uses (Kleene’s strong three-valued logic), so CAN for one standpoint can be IS for another. Cross-standpoint federated queries lift to the four-valued extension where source-level disagreement surfaces as .

Standpoints — what they are

A standpoint is a named view over the model. Two standpoints can disagree about whether a fact holds, what its provenance is, or whether a derivation fires — without either being wrong. The runtime keeps them simultaneously and lets the same query answer differently from each.

Declare standpoints in [standpoints]:

[standpoints]
default = []
tenant_view = ["default"]
landlord_view = ["default"]
auditor_view = ["tenant_view", "landlord_view"]

Each entry is name = [parents] — the standpoint’s parents in the lattice. Children inherit from parents; children can add facts, override defaults, and disagree with parents on derivations. The lattice is a partial order, not a partition: a fact in default is also visible in tenant_view (unless tenant_view retracts it); a fact added in tenant_view is not visible in landlord_view.

This is the load-bearing distinction: standpoints discriminate, they do not partition. A query without a standpoint flag runs against default. A query with --standpoint tenant_view runs against the join of default and tenant_view-local facts. Multiple standpoints active at once is admitted; the engine answers by lattice meet.

Standpoint-discriminated rules

Inside an [modules] block, a module can declare itself contributing to a particular standpoint:

[modules.tenant_only]
path = "tenant_only"
standpoint = "tenant_view"
default-world = "open"

A rule defined in tenant_only.ar is visible only when querying from tenant_view (or its descendants). The same predicate can have different rules across standpoints — for example, MonthlyOwed in tenant_view could include security-deposit interest, while in landlord_view it could not.

This is how regulatory layering works in practice: a federal rule lands in one standpoint, a state-specific rule in a child standpoint, and a query at the state-specific standpoint sees the conjunction. The engine’s stratified semantics keeps the layers honest.

World assumption per (module, predicate)

Open-world assumption (OWA) is the default for ontology work — the absence of IS(p, q) does not mean NOT(p, q). Closed-world (CWA) is the default for computational reasoning — what is not derivable is treated as false.

Argon refuses to pick one globally. Instead, world assumption is configured per (module, predicate):

[modules.tenant_only.default-world]
default = "open"

[modules.tenant_only.world]
ActiveLease = "closed"
HasGuarantor = "open"

Within tenant_only, the predicate ActiveLease runs under CWA — if the engine cannot derive ActiveLease(l), the answer is NOT(ActiveLease, l). HasGuarantor runs under OWA — failure to derive means we don’t know yet. The runtime tracks which assumption applied to each derived fact and surfaces it via the why-provenance.

CWA in an OWA module triggers OW1008 — a warning, not an error. The mix is sometimes intentional (regulatory queries sometimes want CWA for “did the tenant make all 12 payments” while staying OWA for “is there a guarantor we don’t know about”).

Bitemporal axes

Every fact in the runtime carries two timestamps. The pair-of-timestamps design follows Snodgrass & Ahn’s Temporal Databases (1986) and the bitemporal model formalized by Jensen, Soo & Snodgrass in Unifying temporal data models via a conceptual model (1994). Argon’s specific commitment: retraction is a new event with later transaction time, never an in-place modification.

  • Valid-time — when the fact is true in the world. The lease is active from 2026-01-01 to 2027-01-01, regardless of when the system learned about it.
  • Transaction-time — when the system learned the fact. The lease was recorded on 2025-12-14. If the system was updated three days later to correct the rent figure, the new fact has the same valid-time interval but a later transaction-time.

Formally, the event log is a sequence

where is the event payload, its valid-time interval, its transaction-time, the principal who produced it (e.g., --principal alice-001), and the standpoint it landed in. The log is monotonically ordered by : appends never reorder.

The query semantics is a filter over the log. Given a query , a target valid-time , and a target transaction-time :

The query runs against the subset of events whose valid-time is at-or-before , whose transaction-time is at-or-before , and whose standpoint is at-or-below in the lattice. Both axes are recorded for every event in the append-only axiom-event log. Both are queryable independently.

--as-of-valid <RFC-3339> answers a question against the world as it was at the named valid time:

$ ox query active_leases --as-of-valid 2026-06-01T00:00:00Z

returns the leases that were active on June 1, 2026, even if some of them have since been terminated.

--as-of-tx <RFC-3339> answers as-of the system’s knowledge at a particular transaction time:

$ ox query active_leases --as-of-tx 2025-12-14T00:00:00Z

returns the leases the system knew about on December 14, 2025 — even if subsequent corrections changed the picture.

The two flags compose: --as-of-valid X --as-of-tx Y answers “what did the system, as of Y, believe was true on X?” — useful for audit reconstruction.

Retraction as a new event

A fact is never modified in place. A correction is a new event — same valid-time, later transaction-time, marked as a retraction of the prior fact. The old fact stays in the log; queries before the retraction’s transaction-time still see it.

pub mutation retract_lease_signing(l: Lease, retracted_at: Date) -> Lease {
    do {
        retract LeaseSigned(l, _)
    }
    emit LeaseSigningRetracted(l, retracted_at)
    return l
}

The retract clause does not delete: it appends a retraction event whose tx-time is now. A --as-of-tx query before now still sees the original signing; after now, it sees the retraction and LeaseSigned(l, _) is no longer derivable.

This is what makes Argon’s bitemporal semantics audit-friendly: the history of corrections is itself queryable. A regulator can ask “when did you learn that this fact was false?” and get a precise answer.

Forks — branching the world

Sometimes you want to ask a counterfactual: “what would the model look like if we mutated l1 instead of l2?” or “what does the model say if we add this hypothetical regulation?” Forking lets you branch the runtime structurally.

A fork is a copy-on-write branch of the event log. Reads against a fork see the parent’s state up to the fork point plus any fork-local events. Writes to a fork stay local; the parent does not see them. Forks can be nested.

Today, forks are created programmatically through the kernel API; once a fork exists, queries discriminate to it via --fork:

$ ox query active_leases --fork what-if-rent-control --as-of-valid 2026-06-01T00:00:00Z

The structural-sharing implementation means a fork is cheap — it does not duplicate the data, it shares it under the hood. Useful for the long-running “what-if” analyses that ontology projects keep running for months. A CLI surface for creating forks (ox fork) is on the roadmap; until it lands, forks are minted via the kernel API and the application layer that owns the runtime instance.

Multi-axis queries

Standpoint, valid-time, transaction-time, fork — four orthogonal dimensions, each with its own query syntax:

$ ox query active_leases \
    --standpoint tenant_view \
    --as-of-valid 2026-06-01T00:00:00Z \
    --as-of-tx 2025-12-14T00:00:00Z \
    --fork what-if-rent-control

The engine answers from the conjunction: rules in tenant_view, the world as it stood at June 1 (valid-time), what the system knew on December 14 (transaction-time), in the rent-control fork. Provenance comes back tagged with all four axes; you can trace which derivation came from which standpoint, what tx-time the underlying fact has, and which fork it lives in.

The bridge-rule mechanism lets a derivation in one standpoint demand-drive results in a parent standpoint: a fact derived in auditor_view can pull through facts from both tenant_view and landlord_view because auditor_view is the meet of both in the lattice. The engine resolves the join lazily — only the standpoints actually needed by the query get evaluated.

Per-tenant scoping

In multi-tenant deployments — the typical case for a Sharpe-internal ontology runtime — standpoints carry an additional axis: the tenant. Tenant-local standpoints are scoped so that one tenant’s default does not see another tenant’s facts. The configuration lives in the runtime’s tenant manifest, not in the package’s ox.toml; from the language’s point of view, a tenant is an outermost standpoint that wraps everything.

A package author writes against the default standpoint and the lattice declared in [standpoints]. The runtime layers tenant-scoping over that. The query syntax is unchanged.

Worked example: cross-package standpoints

The _catalog/manifest-modules/ workspace family exercises the [standpoints] + [modules] machinery in isolation. Five variants ship:

  • minimal/ — single standpoint (default), single module — the baseline.
  • nested-lattice/ — three-level lattice (default → tenant_view → tenant_admin_view) showing inheritance and meet semantics.
  • cross-package/ — two packages, each contributing rules to the same standpoint via separate modules; resolves under the direct-dependencies rule from Chapter 4.1.
  • mixed-world/default-world = "open" at module top with per-predicate closed overrides; surfaces OW1008 for the intentional CWA-in-OWA mix.
  • negative-cycle/ — cyclic [standpoints] declaration that triggers OE1001.

Each variant ships a runnable ox check so you can see the diagnostic codes from the table above fire on real input rather than reading the codes in isolation.

Diagnostics worth knowing

CodeWhen
OE1001Cyclic standpoint dependency in [standpoints]
OE1002Unknown defeat-ordering strategy
OE1004Module references an undeclared standpoint
OW1008CWA concept inside an OWA module (intentional mix surfaced)
OW1009Standpoint DAG has no root (often a configuration mistake)
OE1010Bridge rule references a private concept
OE1011Module discriminator collision (two modules contribute to the same standpoint with conflicting names)

ox explain <code> prints the long form.

How it composes with the rest of the language

The bitemporal axes interact with mutations: every mutation event lands in the log with the current tx-time and the valid-time the mutation declares. Mutations in Chapter 2.5 are the surface; the bitemporal log is the substrate.

The standpoint mechanism interacts with the meta-property calculus: each standpoint can have its own world-assumption configuration, and the calculus runs once per standpoint when needed. Refinement-type membership under OWA uses (Kleene’s strong three-valued logic), and CAN for one standpoint can be IS for another. Each standpoint also declares a consistency policy (strict default; paraconsistent opt-in) controlling within-standpoint append-time invariants — strict standpoints reject conflicting writes, paraconsistent standpoints persist them as -valued cells in the FDE fragment. Cross-standpoint disagreement is preserved at federation time regardless of policy.

Forks interact with mutations: a mutation against a fork is fork-local; a mutation against default propagates to all forks (because forks share the event log up to their branch point). The “what-if” pattern usually works the other way — fork first, mutate, query — and the pattern is the kind of thing that makes the bitemporal+standpoint+fork triple worth the design budget.

What’s next

This is the last of the Part 5 chapters. The book proper is complete: Parts 1–4 teach the language end-to-end, Part 5 covers the formal foundations the working programmer can use without studying directly. The reference Appendix A (keywords) and Appendix B (operators) collect the syntax surface; Appendix C is the diagnostic registry; Appendix D records where the language is moving; Appendix E is the curated decision log.

The future Argon Book editions will fill in the gaps Part 5’s chapters flagged: the tier-4-and-above execution side as it lands, the multi-axis-query semantics as the implementation matures, and the worked examples from real UFO-team projects as they mature.