---
type: reference
status: locked
requirement: GOV-03
phase: 88-v1-7-1-b-gov
plan: 88-02
tags: [GOV-03, bi-temporal, vault-query, axis-semantics, valid-time, transaction-time, orthogonal, SQL-2011, graphiti]
decided: 2026-06-02
supersedes: "vault-query.js applyBiTemporalFilter L217-231 'earliest of the two' invalidation-reference framing"
---

# GOV-03 — Cross-Axis Bi-Temporal Axis-Semantics Ruling

**Ruling (LOCKED, critic-confirmed): the bi-temporal axes are ORTHOGONAL.** A
valid-time question must never be answered with a transaction-time flag. The fix
is applied in `bin/lib/vault-query.js::applyBiTemporalFilter` (Phase 88, Plan
88-02, Wave 1).

## The two axes (SQL:2011 / graphiti)

Bi-temporal data models carry two **independent** time axes:

- **Valid time** — *when a fact is true in the modelled world.* Anchored by
  `valid_from` (onset) and `invalidated_at` (offset). The query lever is
  `--as-of <X>` ("what was true on date X?").
- **Transaction time** — *when the system recorded/believed a fact.* Anchored by
  `recorded_at`. The query lever is `--recorded-as-of <X>` ("what did the system
  believe on date X?").

Per SQL:2011 (system-versioned + application-time-period tables) and the graphiti
temporal-knowledge-graph model, these axes are **orthogonal**: a value on one axis
does not move a reference on the other. You can ask "what was true on 2025-06-01?"
(valid time) entirely independently of "what did the system know on 2026-01-01?"
(transaction time), and any combination is a *set-intersection*, never a
cross-axis blend of reference instants.

## The defect (audit #13, DEFECT/LOW)

In `applyBiTemporalFilter`, the default-exclusion gate (a) excludes a hit when it
is invalidated **at the reference instant** (`isInvalidatedAtRef`: `invalidated_at
<= referenceMs`). `referenceMs` was computed as the *earliest of three* instants:

```js
let referenceMs = nowMs;
if (asOf !== null)        referenceMs = Math.min(referenceMs, asOf.getTime());        // L230 — KEEP
if (recordedAsOf !== null) referenceMs = Math.min(referenceMs, recordedAsOf.getTime()); // L231 — DROP
```

`invalidated_at` is a **valid-time** property. Folding the **transaction-time**
flag (`recordedAsOf`) into its reference instant is a cross-axis leak: a
transaction-time query could drag the valid-time reference *backward*, before a
note's invalidation, and thereby **un-invalidate a note that is invalidated as of
now**. Asking "what did the system know on 2026-01-01?" must NOT resurrect a note
that became invalid on 2026-06-01.

## The fix (critic-confirmed locality — one deletion, no ripple)

**Drop ONLY the L231 `recordedAsOf` term. KEEP the L230 `asOf` term.**

- `referenceMs` is now **valid-time only**: `now()` by default, or `--as-of` for
  legitimate valid-time travel (documented L222-227; dropping L230 too would
  *regress* valid-time travel — a different defect).
- **No ripple.** `referenceMs` is consumed ONLY by `isInvalidatedAtRef` → ONLY by
  gate (a) at L245. The marker path `isInvalidatedNow` (drives the `__bitemporal_
  invalidated` / `kind: "invalidated"` marker) already uses `nowMs` independently.
  Block (c) (the `--recorded-as-of` transaction-time filter: `recorded_at <=
  recordedAsOf`) never touches `referenceMs` — transaction-time travel stays
  block (c)'s job, exactly where it belongs.
- **Fail-open preserved.** Malformed `invalidated_at` / `valid_from` → `Date.parse`
  → `NaN`; `NaN <= ref` is always `false` → treated as still-valid (SPEC §4).
  Untouched by this fix.

The header comment at L217-228 was reconciled in lockstep (doc↔code parity): it no
longer describes "earliest of the two" as the invalidation reference; it states the
reference is valid-time only and that `--recorded-as-of` is purely block (c)'s
transaction-time filter.

## The demonstration (the RED → GREEN fixture)

`bin/lib/vault-query.test.cjs`, **Arm 1 (the sharp arm)** — `--recorded-as-of`
**ALONE**, `as_of` unset:

- Note: `invalidated_at = 2026-06-01` (strictly **between** the recorded-as-of
  instant `2026-01-01` and `now()` ≥ `2026-06-02`), `recorded_at = 2025-12-01`
  (so block (c) keeps it).
- **Before the fix (RED):** `referenceMs = min(now, recordedAsOf=2026-01-01)`;
  `invalidated_at 2026-06-01 > 2026-01-01` → `isInvalidatedAtRef = false` → the
  note is **wrongly KEPT** (a transaction-time flag moved the valid-time
  reference back before the invalidation).
- **After the fix (GREEN):** `referenceMs = now() = 2026-06-02`; `invalidated_at
  2026-06-01 <= now` → `isInvalidatedAtRef = true` → gate (a) **EXCLUDES** it.
  The note IS invalidated as of now; a transaction-time question cannot
  un-invalidate it.

Supporting arms (all green pre- and post-fix): **Arm 2** proves L230 was *kept*
(valid-time travel still keeps a note valid as-of the `--as-of` instant); **Arm 3**
proves fail-open is intact (malformed `invalidated_at` → kept); **Arm 4** proves the
combined-flag `(b)+(c)` set-intersection is unchanged.

## Sequencing — why this lands in Phase 88, Wave 1

GOV-03's fix region is the **same bi-temporal region** that Phase 89 REFACTOR-02
extracts into `vault-bitemporal.js`. Recording this ruling and committing the fix
*before* REFACTOR-02 means the extracted module lifts **already-correct** code and
encodes the resolved (orthogonal) semantics from the start — rather than carrying
the cross-axis defect into a fresh module. This is why GOV-03 is sequenced into
Phase 88 ahead of the extraction.

## Status

- **Ruling:** LOCKED — axes are orthogonal; apply the fix.
- **Fix:** APPLIED (Plan 88-02, Task 2). L231 dropped; L230 kept; header reconciled.
- **Evidence:** `bin/lib/vault-query.test.cjs` Arm 1 RED→GREEN; Arms 2/3/4 green.
- **Mirror:** `bin/lib/vault-query.js` + `.test.cjs` + this ruling mirrored
  byte-identically to `get-magic-done/` (TD-07).
