Measured position
Where a node stands in the field: three numbers computed by an encoder reading the corpus text, never read off a declared hierarchy.
Measured position is the coordinate a node holds in the field, computed by an encoder reading what the corpus says rather than read off a hierarchy somebody typed. Every node is folded into one document, the documents are weighted and reduced to three directions, and the result is rotated onto a basis committed to disk. The encoder is never shown a declared edge: deleting every edge declared between the site’s own pages moves no position at all, and subjects that declare an edge still land measurably nearer each other than subjects that do not.
The coordinate
A coordinate is three numbers and a list. Coord in src/field/place.ts carries xyz, the
three axes the camera travels, and rest, the axes it cannot. rest is empty in every
placement the site ships, and is the one place a fourth number can arrive later without the
page payload changing shape.
The coordinate is opaque by construction. No renderer, route or index reads a component for meaning, so an axis is free to stop being the axis it was on the previous build without anything downstream having to be told. What consumes a position is the medium, which reads a point and a mass and nothing else about where either came from.
A provider is one way of deciding where things stand. placeField asks each provider in
turn and takes the first placement offered that names at least one node. Three properties
follow from that shape:
- A provider declines rather than throws. Returning null is the ordinary way of saying it has no answer, and a provider that throws anyway is caught and treated as one that declined.
- The answer is attributed, never branched on. A placement carries a
sourcestring naming the provider that produced it. Pages report it; nothing reads it to decide anything. - A node nobody placed still gets a point.
positions()seeds it from a hash of its own id, because an undefined coordinate reaches the runtime as a label that never places and a camera that flies to a non-finite point.
Positions are normalised into a fixed box, FIELD_EXTENT, whose half-extents are 1.75 wide,
1.05 tall and 1.35 deep. One scale serves all three axes, taken from whichever axis runs out
of box first, because scaling them separately would shear the field.
The encoder
src/field/embed.ts is the first provider, and it runs in five stages.
- Documents.
docsForfolds each node into one document by the same rules the graph uses to fold mass: a corpus subject takes its own article plus every signed reading in its folder, a persona takes its own file, and a place, which has no file of its own, takes its title and gloss. A document of fewer than 20 tokens has the title and gloss appended. - Tokenising. Code fences, inline code, link targets and HTML tags are stripped before the text is split, so a subject quoting the file it is about is not placed by that file’s identifiers. Tokens under 3 or over 24 characters are dropped, and a fixed inline stoplist removes the common words that survive the length rule.
- Weighting. Each token is hashed into one of 65,536 columns; no dictionary is kept and
collisions are accepted. A column’s weight is
(1 + log tf) * log((N + 1) / (df + 1))and each document’s row is normalised to unit length. A term every document uses weighs exactly zero and is dropped, which is what makes the stoplist a convenience rather than a load-bearing list. - Reduction. A randomised SVD, not a full one. The sketch carries 8 columns, three kept directions plus five oversampled, refined by two power iterations with a modified Gram-Schmidt after each. Only the 8 by 8 Gram matrix is ever formed, and its eigenpairs come from a cyclic Jacobi sweep. A node’s coordinate is its left singular vector scaled by the singular value, which is what puts the strong direction further across the box than the weak one.
- Sign. An eigenvector has no sign, so the reducer is free to hand back the mirror of the same picture. Each axis is pinned to the sign of the document loading hardest on it, making the choice a fact about the corpus rather than about the run.
| Constant | Value | What it fixes |
|---|---|---|
BUCKETS | 65536 | Columns in the hashed term space |
RANK | 3 | Directions kept, one per axis the camera travels |
OVERSAMPLE | 5 | Extra sketch directions, so the kept three are clean |
POWER | 2 | Power iterations in the range finder |
EMBED_VERSION | 1 | Bumped when the arithmetic changes; an older cache is ignored |
Determinism is the property everything downstream rests on, and the module is written to
enforce it. There is no clock, no Math.random, no network and no DOM anywhere in the module.
Ids are sorted before every sum, so the order floating-point error accumulates in is fixed.
The projection matrix is never materialised: each column’s row is regenerated from the column
number. The gaussian is Irwin-Hall, twelve draws minus six, because Box-Muller would put a
logarithm and a cosine in the one part of the file that has to be bit-stable. The Jacobi
rotation angle is taken algebraically so the routine contains a square root and no inverse
trigonometry. Math.log is quantised at every call, being the only function used that the
language does not require to be correctly rounded. scripts/test-field.mjs asserts that two
runs in one process produce identical output, and that a recompute matches the pre-alignment
digest committed alongside the basis.
Declared edges under the encoder
A hierarchy that came from a directory tree or from frontmatter is a hierarchy somebody typed. The encoder reads prose and receives no edge list, and the test measures that in both directions on the live corpus.
| Provider | Effect of removing the 11 edges declared between the site’s own pages |
|---|---|
| Encoder | No node moves. The assertion is exact equality to zero, not a tolerance. |
| Spring settle | Every node moves. The assertion is a greatest move above 0.5, which is most of a box half-extent. |
The recovery is the other half of the same measurement. Subjects joined by a declared see:
edge land nearer each other than subjects with no edge between them, without the encoder ever
being shown one. The test takes the mean distance over every declared pair in the corpus
against the mean over every pair with no edge between them, and divides the first by the
second.
That ratio is held inside a band rather than at a target, because it moves with the corpus. A ratio near 1 would mean the encoder collapsed and every document landed in the same place; a ratio near 0 would mean the edge list leaked into the documents it is supposed to know nothing about. The test holds it between 0.2 and 0.55, and the band is wide because the figure is also sensitive to the tokeniser.
The committed basis and the drift bound
Re-embedding moves everything. An encoder run over changed prose produces a different basis,
every position changes with it, and a reader’s memory of where a subject sits dies on deploy.
src/field/embedding.json is the answer: one basis, committed rather than derived, that each
fresh run is rotated onto before it ships.
The fit is the orthogonal transform carrying the fresh embedding onto the committed one, computed over every id the two share. The anchors are not curated. Curation was tried and lost, because the obvious candidates, the places and the personas, are the shortest documents in the corpus and therefore the least stably placed of all of them. The transform is fitted on the shared ids and applied to all of them, so a node that arrived since the cache was written lands in the same frame as its neighbours. With nothing to fit against, a fresh embedding is returned untouched: a first run is its own basis by definition.
The determinant of the fit is reported and never corrected. A reflected basis is a change of basis rather than a change of content, and forcing a proper rotation to satisfy a convention costs an order of magnitude of residual for nothing.
Drift is reported twice, raw and aligned, because only the difference between them says which thing happened: a large raw drift with a small aligned one is a basis that turned, and a large aligned one is prose that changed. Two bounds sit on the aligned figure.
| Bound | Value | What it measures |
|---|---|---|
DRIFT_MAX | 0.2 | The furthest any shared id moved after alignment |
DRIFT_MEAN | 0.08 | The mean move over every shared id |
Both are measured before rounding and before normalisation, and the test re-checks the same bound in the units a reader sees by multiplying each by the frozen box scale. The box, meaning the centre, the single scale and the axis order, is written into the cache and reused rather than re-derived. Without that the normaliser is an amplifier: it takes its scale from whichever node is furthest out, so one article growing enough to push a single node outward would move every other node by a distance nothing measured.
Exceeding the bound is not by itself a bug. Three articles rewritten at once move the corpus
for real and no alignment hides that. The response is a deliberate
node scripts/build-embedding.mjs --write, which makes the current run the committed basis;
later runs then align onto that. A run without --write reports and exits non-zero, and never
writes a half-built embedding over a good one.
The direction of the fit is the one mistake here that degrades the picture quietly instead of
loudly. R = V U^T carries the fresh run onto the committed one; its transpose is the inverse
and leaves the picture further from the cache than doing nothing at all, which no drift bound
loose enough to survive real editing would notice. The test fits a synthetic rotation with a
known answer and asserts the residual is effectively zero.
The fallback provider
src/field/field.ts holds the provider order and is the only module that knows there is more
than one: the encoder first, the spring settle in src/field/layout.ts behind it. The settle
is the previous layout, unchanged and still exact. It runs a fixed 900 steps from a seeded
start with no clock and no entropy, reads the declared edges twice, once for a repulsion
radius and once for spring rest lengths, and never declines, so a list ending in it always
places the field.
The encoder declines in three conditions and no others:
- A graph of fewer than 4 nodes, which is too small to reduce.
- A coordinate that came back non-finite after normalisation.
- A throw from anywhere inside the encoder.
A missing or out-of-date embedding.json is not one of them. A cache that is empty, or
written by a version the code does not understand, leaves the encoder placing the field in its
own basis rather than the committed one. That costs a reader’s memory of where things were and
nothing else, so it ships in preference to the settle. The current build is placed by the
encoder and reports embed.
The placement is computed once for the whole build. Astro re-runs a layout’s frontmatter on every page, so the memo lives in a module instead, which also lets the build script and the test ask for the same field the pages get.
What the coordinate does not yet measure
The design this implements calls for a coordinate taken from a node’s content and from its
outcomes. Only the first half runs. Nothing under src/field reads an outcome record, and the
encoder’s entire input is corpus text plus the graph’s own titles and glosses.
The outcome record is written into one visitor’s own
browser storage and is read back only there, by the structure pool that produced it. A static
build has no records available to it at all, so
behaviour space remains a separate space over outcome
vectors from which no field coordinate is derived. Joining the two is what the rest field on
a coordinate exists to receive, and it is empty in every placement shipped today.
Bound to
The source this page's claims were read against, named by symbol and pinned by the digest of the file it lives in.
| Symbol | Digest |
|---|---|
| candidate | unbound |
| embedCorpus | unbound |
| embedding.json | unbound |
| PLACE_EDGES | unbound |
| placeField | unbound |
| readRecords | unbound |
| SETTLE | unbound |
| test-field.mjs | unbound |
| theField | unbound |
What links here
- The elite archive sends readers here
- The medium sends readers here
- The outcome record sends readers here
Near this subject
These are the subjects this one is joined to in the field. Step out of this card and they are the wells around the one you are standing in.
- The elite archive that page sends you here
- The outcome record each sends you to the other
- The medium each sends you to the other
- Behaviour space this page sends you there