Skip to content

PointSav Documentation

The engineering library for the PointSav platform — operating systems and services for regulated businesses that own their data, their AI, and their record-keeping outright. Where the monorepo holds the code, this wiki holds the reasoning: architecture, services, security, and the governance commitments that bind future development.

Identity ledger schema design

← All revisions

417a31fa · PointSav Digital Systems ·

security/: replace 12 articles with fresh-draft-first pilot rewrites against schema-topic.yaml

View the full record as of this revision →

@@ -1,145 +1,188 @@
---
schema: foundry-doc-v1
title: "Identity ledger schema design"
slug: identity-ledger-schema-design
category: security
type: topic
content_type: topic
slug: identity-ledger-schema-design
title: "Identity ledger schema design"
short_description: "The identity ledger defines three record types — Person, Anchor, Claim — representing who is known, how identity was observed, and what was recorded, via WORM."
quality: complete
status: active
audience: vendor-public
bcsc_class: current-fact
language: en
language_protocol: PROSE-TOPIC
last_edited: 2026-08-03
editor: pointsav-engineering
short_description: "Three record types — Person, Anchor, Claim — separate who is known from how they were observed and what was asserted. Identity is a UUIDv5 of a lowercased email, so the same input always yields the same identifier."
paired_with: identity-ledger-schema-design.es.md
category: security
status: active
quality: complete
last_edited: 2026-06-23
---

The identity ledger is the deterministic identity primitive at the centre of Ring 1 data ingest. It defines three record types — Person, Anchor, and Claim — that together represent who is known to the system, how that identity was observed, and what attributes have been recorded about it. All three write through `service-fs`, producing an immutable, append-only audit trail with no AI involvement at any stage. See also [[worm-ledger-design|the WORM ledger design]], [[three-ring-architecture]], and [[machine-based-auth|machine-based authorization]].

## Key Takeaways

- Identity resolution is fully deterministic. The primary identifier is a UUIDv5 derived from the lowercase-normalised primary email address — the same email always produces the same UUID on any machine, at any time, with no AI or probabilistic matching. This satisfies SYS-ADR-07's requirement that Ring 1 extraction bypass inference.
- Identity records follow [[worm-ledger-architecture|WORM discipline]]. All three record types are written through `service-fs` as an append-only audit trail. Records cannot be deleted or modified after the fact.
- A single Person record is the authoritative source for a known identity. Downstream services resolve identity from this record — they do not derive or cache their own.
- Ambiguous or conflicting identities are surfaced to the system operator rather than silently merged. Deterministic extraction at Ring 1; human resolution for exceptions. No probabilistic model makes silent merges at ingest.

## 1. The Identity Primitive

Every identity in Ring 1 is a UUID version 5 derived from a lowercase-normalised email address:
**The identity ledger schema** is the three-record model this platform uses to record who is known
to it: a **Person** record establishing that an identity exists, an **Anchor** record recording
where and when that identity was observed, and a **Claim** record asserting one attribute about it
with a stated confidence and source. Separating the three keeps a durable identity from being
overwritten every time a new document mentions it, and keeps a disputed attribute from
contaminating the identity it describes. Its defining design decision is that identity resolution
involves no inference of any kind: the primary identifier is derived arithmetically from an email
address, and observations are extracted by a fixed regular expression, never a model.

The separation solves a specific problem. A single mutable person row forces every new observation
to be either merged into the existing record or discarded, and merging is where identity systems
accumulate silent, unattributable error. Under this model an observation adds an Anchor, an
assertion adds a Claim, and neither alters the Person. Disagreement between sources becomes two
Claims with different sources rather than a lost prior value. Every structural claim in this
article has been verified directly against the current canonical source of the implementing
service, [[service-people|service-people]].

## The three records

**Person** carries seven fields: the derived `id`, a display `name`, the lowercase-normalised
`primary_email`, a list of `email_aliases` (also normalised on entry), an optional `organisation`,
and creation and update timestamps. The identifier is never caller-assigned — the only constructor
derives it from the email, so a Person whose `id` disagrees with its address cannot be built.

**Anchor** carries three fields: the `target_uuid` it points at, the observed address as
`anchor_source`, and a timestamp. An Anchor deliberately does not assert that the address belongs
to any named individual — it records only that the address was observed and what identifier
corresponds to it. Anchors are append-only; the system never modifies or retracts one.

**Claim** carries seven fields: a `claim_id` (a random UUIDv4, unique per observation — unlike the
derived identity UUID), the `target_uuid` it annotates, an `attribute` name and observed `value`, a
`confidence_score`, a `source_id` recording where the observation came from, and a timestamp. In
the sanctioned extraction path the confidence score is `1.0` for every Claim without exception,
because the only extraction method wired to it is a deterministic regular expression scanning for
email addresses; the field exists for possible future extraction methods, which would remain
subject to the no-inference boundary described below.

The asymmetry is the design. Person is the stable object; Anchor and Claim both point *at* a person
identifier and neither can modify it. Confidence lives on the Claim, where a contested assertion
belongs, and never on the Person.

### Deterministic identity

The person identifier is a version-5 UUID derived from the lowercased primary email address under
the standard DNS namespace:

```
id = UUIDv5(NAMESPACE_DNS, lowercase(primary_email))
```

This derivation is deterministic: the same email address always produces the same UUID on any machine, in any language, at any time. There is no random seed, no AI classification, no lookup against an external service. Two systems that independently ingest the same email address will arrive at the same UUID.

This property is the foundation of the identity primitive's composability guarantee: a UUID produced by `service-people` can be referenced by `service-email`, `service-input`, or any Ring 2 component without a shared identity registry. The derivation is the registry.

## 2. The Person Record

The Person record is the primary identity object. It is created by the `identity.append` MCP tool and stored in the `service-people` in-process `PeopleStore`, then written through to `service-fs`.

```
Person {
    id:              UUIDv5(NAMESPACE_DNS, lowercase(primary_email))
    name:            String
    primary_email:   String  // always lowercase-normalised
    email_aliases:   Vec<String>
    organisation:    Option<String>
    created_at:      DateTime<UTC>  // RFC3339
    updated_at:      DateTime<UTC>  // RFC3339
}
```

The `id` field is always derived; it is never assigned by the caller. The `primary_email` field is always stored in lowercase regardless of how it was supplied. These invariants are enforced in `Person::new()` — there is no constructor that bypasses them.

## 3. The Anchor Record

An Anchor is an immutable observation that an email address was seen in a specific context. It is produced automatically by the `identity.scan_text` MCP tool when an email regex match is found in a block of text.

```
Anchor {
    target_uuid:   String  // UUIDv5(NAMESPACE_DNS, lowercase(email))
    anchor_source: String  // the email address as observed
    timestamp:     String  // RFC3339
}
```

An Anchor does not assert that the email belongs to a named individual. It records the observation of an email address and derives the UUID that would correspond to that address. Anchors are strictly append-only: the system never modifies or retracts an Anchor once written.

## 4. The Claim Record

A Claim is an attribute observation with provenance. It is produced by `identity.scan_text` alongside each Anchor and records what attribute was inferred and from what source.

```
Claim {
    claim_id:         String  // UUIDv4 — unique per invocation
    target_uuid:      String  // the identity being annotated
    attribute:        String  // e.g. "email"
    value:            String  // the observed value
    confidence_score: f32     // 1.0 for regex-verified; <1.0 reserved for future
    source_id:        String  // caller-supplied provenance identifier
    timestamp:        String  // RFC3339
}
```

The `claim_id` is a UUIDv4 (random, not derived) — it is unique per Claim invocation. The `confidence_score` is `1.0` for all Claims produced by the current implementation because the only extraction method in use is a deterministic regular expression. A score below `1.0` is reserved for future extraction methods operating under SYS-ADR-07 constraints.

## 5. Conflict Detection

When a Person record is appended to `PeopleStore`, the store checks whether any email in the record (primary or alias) is already bound to a different UUID. If so, the store returns a `ConflictingIdentity` error rather than silently merging or overwriting.

```
PeopleStoreError::ConflictingIdentity {
    email:       String  // the email that triggered the conflict
    existing_id: Uuid    // the UUID already bound to that email
    new_id:      Uuid    // the UUID that the new record would assign
}
```

Conflicts are surfaced to the caller — they are never automatically resolved. This implements the F12 requirement (SYS-ADR-10): when deterministic matching surfaces ambiguity, the system surfaces it to the operator rather than resolving it with heuristics or AI inference.

## 6. SYS-ADR-07 Compliance: Zero AI in Ring 1

The identity ledger schema is designed so that every operation is deterministic and verifiable without model state:

- Email extraction uses a single compiled regular expression (`(?i)[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}`).
- UUID derivation uses the UUIDv5 algorithm (SHA-1 with a fixed namespace UUID — deterministic, not learned).
- Confidence scores are assigned by rule (`1.0` for regex match), not by a model.
- Conflict detection compares UUIDs by equality — no fuzzy matching, no embedding similarity, no threshold tuning.

No model weights are present in `service-people`. No network call is made to an inference endpoint during identity ingest. These are hard constraints of SYS-ADR-07, not implementation preferences.

## 7. WORM Persistence

All three record types — Person, Anchor, and Claim — are persisted by writing through to `service-fs` via `FsClient`. The write path uses:

- HTTP POST to `/v1/append` on the service-fs instance
- `X-Foundry-Module-ID` header set to the value of `PEOPLE_MODULE_ID` (environment variable, e.g. `foundry-workspace`)
- JSON-serialised record body

Every identity observation is written to the WORM ledger at the moment it is ingested. The records cannot be deleted or modified after the fact. The append-only guarantee comes from service-fs's D4 atomic-write discipline and linear SHA-256 hash chain.

Three write paths exist:

1. `identity.append` (MCP tool) — writes a Person record
2. `identity.scan_text` (MCP tool) — writes one Anchor and one or more Claims per email address found in the input text
3. No direct write path for Anchor or Claim in isolation — they are always produced as a pair by `scan_text`

## 8. Forward-looking: Ring 2 Identity Extension

The schema described above is the Ring 1 deterministic baseline. Planned Ring 2 extensions include:

- **Cross-tenant identity sharing** — a query interface intended to look up identities across module boundaries, for use by Ring 2 components such as `service-extraction` that need to correlate entities observed in different ingestion contexts.
- **Embedding-based fuzzy matching** — an optional similarity layer, intended to run in Ring 2, that may suggest candidate identity merges for operator review. This is gated by SYS-ADR-07: any inference-based suggestion must surface to the operator for confirmation; it may not write to the Ring 1 WORM ledger without explicit operator action.

These extensions are planned and intended; they are not present in the current implementation.
The constructor lowercases before hashing, and a unit test asserts case-insensitivity explicitly.
The property this buys is reproducibility without coordination: the same email address yields the
same identifier on any machine, at any time, with no shared counter, no allocation service, and no
probabilistic matching — two components that have never communicated will independently arrive at
the same identifier for the same person, because *the derivation is the registry*, not a lookup
against it.

The two UUID versions in the schema divide labour deliberately. Identity uses version 5 — a
namespaced hash — precisely *because* it is deterministic: identity must be reproducible from the
address alone. Claims use version 4 — random — precisely because they must not be: each observation
is a distinct event, and two observations of the same attribute from the same source are two
records, not one.

The cost is equally definite and belongs alongside the benefit. Identity is bound to one email
address. A person whose primary address changes derives a different identifier until an operator
links the two via the alias list; the schema chooses that visible, auditable cost over silent
inference.

## The write path — and a second, unsanctioned one

The people component writes records to the file service over HTTP: a POST to a `/v1/append`
endpoint carrying an `X-Foundry-Module-ID` header, verified by reading the client directly and by
an end-to-end test that starts a real file-service daemon and asserts a record round-trips
faithfully. Storage on the far side is the hash-chained append-only log described in
[[cryptographic-ledgers]], so identity records inherit its tamper-evidence and its checkpoint and
anchoring properties.

That is the sanctioned path, and stating it plainly matters because it is not the only path in the
tree. A standalone mining tool, `tool-acs-miner`, defines byte-identical Anchor and Claim structures
of its own, derives identifiers with the identical `Uuid::new_v5` call (a test in `service-people`
itself pins agreement between the two implementations), and writes them with ordinary filesystem
calls directly to append-mode files under its own working directory — no HTTP call, no file
service, no hash chain. It also assigns confidence scores that vary by attribute type — `1.0` for
email, `0.9` for phone, `0.6` for a proper-noun match — unlike the sanctioned path's constant
`1.0`. Records written this way are outside the ledger's integrity guarantees entirely. No script
or component in the tree was found to invoke this tool, so whether it runs anywhere could not be
established; it is reported as present rather than as active.

The schema documentation adds a third description again: a JSON schema file and a component README
describe a considerably richer record — a structured `addresses` object holding emails, phone
numbers, and endpoints, a `roles` list, and a metadata block, under field names (`identity_id`,
`addresses.emails`, `addresses.phones`) that no code in the tree writes. That document describes an
intended model, not the implemented one, which remains the seven flat `Person` fields above.

## Conflict handling

The in-process store defines a typed error for conflicting identity, carrying the email address,
the identifier already bound to it, and the newly derived one. An append that would bind an
already-known email to a different identifier is refused rather than merged, and a test exercises
that refusal.

The refusal is real; a phrase sometimes used for it deserves qualification. The conflict is
surfaced as an error returned to the caller of the append operation — in the tool interface, as an
error string on the call. There is no operator inbox, review queue, or dedicated resolution
interface in code. The guarantee that holds is *no silent merge*: the write fails and the caller
learns why. The guarantee that does not hold today is that a person is systematically presented
with the conflict for adjudication — that remains a property of whatever calls the append
operation, not of the schema itself.

## The no-inference boundary and its governance citations

The schema is built so every operation is verifiable without model state. Email extraction is one
fixed, case-insensitive regular expression. Identifier derivation is the UUIDv5 algorithm — a hash
with a fixed namespace, deterministic by definition. Conflict detection is UUID equality — no fuzzy
matching, no embedding similarity, no tuned threshold. There are no model weights in the identity
service and no calls to any inference endpoint on the ingest path; the component's own source
states this directly.

Two distinct governance rules are frequently cited together here and are worth separating, since
conflating them misstates both. **SYS-ADR-07** prohibits routing structured data through an
inference model — it is the rule behind this zero-inference extraction path. **SYS-ADR-10** is the
separate rule requiring a mandatory human checkpoint at commit time; it governs human commitment to
a write, not the absence of a model in the pipeline. They are two rules governing two different
mechanisms, not one compound rule.

Planned extensions sit outside the no-inference boundary by design: a cross-tenant identity query
interface, and an optional similarity layer that may *suggest* candidate merges for operator review.
Both are intended for the platform's second ring; neither exists in the current implementation, and
under the platform's rules any inference-derived suggestion would require explicit operator action
before anything reached the first-ring ledger.

## What this is not

**This is not the queue-based verification flow.** The human-in-the-loop tool described in
[[verification-surveyor]] operates on per-transaction JSON files in a discovery queue and never
touches these record types or the file service. Two systems share the word "identity" and a
directory prefix and share no code.

**The implemented Person record is not the documented one.** The component's JSON schema and README
describe a richer record with structured addresses, roles, and metadata under field names nothing
writes. The implemented record is seven flat fields.

**Not every writer of these record shapes goes through the file service.** The standalone mining
tool writes identical structures directly to local append-only files, outside the hash chain. Any
claim that all identity records are covered by the ledger's integrity properties is true of the
sanctioned path and not of that one.

**Deterministic identity is not identity resolution.** The derivation guarantees that the same
email yields the same identifier. It does not decide whether two different email addresses belong
to the same person, and it will not conclude that two different addresses belong to the same
human — joining them requires a deliberate operator act on the alias list, never an automatic
merge. Treating anchor volume as a fact about an individual would also misread the schema: an
Anchor is not a claim about a person at all, only about an address's occurrence.

**Conflicts are not routed to a person.** They are refused and returned as an error to the caller.
The absence of a silent merge is the delivered property; systematic adjudication by a reviewer is
not implemented.

**Immutability is not a property of these records themselves.** It comes from the file service's
chained, checkpointed, read-only-on-write storage. Records written outside that path — as the
mining tool's are — have none of it.

## See also

- [[cryptographic-ledgers]] — the append-only chained storage the sanctioned write path uses
- [[worm-ledger-design]] — the write-once record model and its guarantees
- [[service-people]] — the component owning the Person, Anchor, and Claim types
- [[verification-surveyor]] — the separate human confirmation step for queued fragments
- [[machine-based-auth]]
- [[capability-based-security]]
- [[worm-ledger-design]]
- [[cryptographic-ledgers]]
- [[service-people]]
- [[three-ring-architecture]] — the layered arrangement in which the archive tier sits
- [[tiered-entity-extraction-architecture]] — the extraction stages producing anchors and claims
Important Information

Corporate structure. PointSav Digital Systems ("PointSav") is currently a trade name of Woodfine Capital Projects Inc. ("Woodfine"), planned to become a wholly-owned Woodfine subsidiary upon incorporation. PointSav does not itself offer, sell, or solicit any security. Any securities offering associated with Woodfine's real-property direct-hold solutions is made exclusively by Woodfine, and only by means of the applicable Private Placement Memorandum.

No investment advice. This wiki's content is provided for engineering, operational, research, and development purposes. Nothing on this wiki constitutes investment advice or a solicitation to invest in any Woodfine partnership or direct-hold solution.

Intellectual property. The PointSav name, trade name, wordmark, and marks, together with all current and future PointSav- and Totebox-branded products, services, and offerings — and the software, source code, documentation, design system, and all related materials — are proprietary to Woodfine and its affiliates, except for components identified as open source. No rights are granted except as expressly set out in a written license or agreement. The full trademark notice appears in the footer of every page on this site.

Open source components. Portions of the platform are made available under permissive open-source licenses identified in the accompanying repository. Use of those components is governed by their respective license terms.

No warranty; informational use. Content on this wiki is provided for general informational purposes only and does not constitute a representation, warranty, or commitment with respect to product functionality, availability, pricing, or roadmap. Some articles describe planned or intended features, capabilities, and milestones — language such as "planned," "intended," "targeted," "may," and "expected" marks this forward-looking content, which is subject to change and does not constitute a commitment regarding future performance.

Confidentiality. Where an article describes an operational or deployment detail that is not intended for public disclosure, that article is not published on this wiki. Content here is general-purpose engineering documentation, not customer-specific configuration.

Jurisdiction. Woodfine Capital Projects Inc. is organized in British Columbia, Canada. References to the Sovereign Data Foundation on this wiki describe a planned or intended initiative only, not a current equity holder or active governance body.

Changes to this notice. PointSav may update this notice from time to time; the version posted on this page governs.

Not a filing system. This wiki is not a securities filing system, an electronic disclosure repository, or a substitute for SEDAR+ or any other regulatory filing system. Formal securities filings are made through the applicable regulatory filing system, not through this wiki.

Full disclaimer. This notice supplements, and does not replace, the full Disclaimers article. In the event of any conflict, the full Disclaimers article governs.

Read the full disclaimer →