Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
36b9f3d
Feature 007: spec + the TS/Python contract for the vacancy transform
jeremymanning Aug 4, 2026
71ce570
Correct the stated reason for the u(stem) departure -- it was not a bug
jeremymanning Aug 4, 2026
8da0cda
Add the swap control, without which the pretrained number means nothing
jeremymanning Aug 4, 2026
e5171a1
Fix two real contract defects the Python implementation exposed
jeremymanning Aug 4, 2026
0007144
Correct the CODAS count and pin how types/stems are counted
jeremymanning Aug 4, 2026
e678665
Pin salt semantics: quality thresholds count attempts, not absolute salt
jeremymanning Aug 4, 2026
5d355ff
Python vacancy transform: the field-without-location instrument
jeremymanning Aug 4, 2026
08f619b
Pin the map's domain to the FULL Dolch list, not the active budget
jeremymanning Aug 4, 2026
a0f8d7b
Put the scope in the name: domainTypes* and corpusTypes*
jeremymanning Aug 4, 2026
ad1c4dc
UI spec for both panels
jeremymanning Aug 4, 2026
ac69fcf
Measure the pretrained arm before building it -- and find the effect …
jeremymanning Aug 4, 2026
4def8a7
TypeScript vacancy transform, built independently from the same contract
jeremymanning Aug 4, 2026
44ea2d1
Python vacancy: scoped counts, pinned domain, salt semantics
jeremymanning Aug 4, 2026
1309d1d
Remove the avoid parameter: the map must not depend on caller memory
jeremymanning Aug 4, 2026
d6e9d5d
A saved Geometry Lab model lost its own words across a reload
jeremymanning Aug 4, 2026
16250db
TS vacancy: implicit domain avoidance, and a vacancyDomain helper
jeremymanning Aug 4, 2026
0c3ef4e
Correct a paragraph my own change had made stale
jeremymanning Aug 4, 2026
9718c47
Python vacancy: implicit domain avoidance, and str guards on type ite…
jeremymanning Aug 4, 2026
8a94ad2
Verified TS/Python parity directly: 6,720 nonces, zero mismatches
jeremymanning Aug 4, 2026
7bc4cb1
Rename VacancyMap.map to .mapping so both stacks agree
jeremymanning Aug 4, 2026
3212403
Point the Spec Kit feature marker at 007
jeremymanning Aug 4, 2026
bbee124
Notes: two shipped defects found while building 007
jeremymanning Aug 4, 2026
405c4ac
Two contract gaps the golden fixture found in the control conditions
jeremymanning Aug 4, 2026
cae0fb8
Golden fixture: TS/Python parity for the vacancy transform, pinned as…
jeremymanning Aug 4, 2026
2e4fb6a
Lexicon Lab: the vacancy panel, and the invariance result made visible
jeremymanning Aug 4, 2026
7163951
Open the corpus view on the verse, not on the book's table of contents
jeremymanning Aug 4, 2026
6f3ec69
The inconsistent control must enforce condition B too
jeremymanning Aug 4, 2026
c115ec6
Stop asking for q4f16: it builds a working-looking session and return…
jeremymanning Aug 4, 2026
0e1a60d
Condition B on the per-occurrence path, and the fixture regenerated
jeremymanning Aug 4, 2026
4c2c3da
Pin `forbidden` as stored, including superseded re-mint nonces
jeremymanning Aug 4, 2026
7dc4918
Notes: the 13 contract defects, and how each was found
jeremymanning Aug 4, 2026
533ab2b
The vacancy transform on the wire: POST /api/lex/vacancy, both stacks
jeremymanning Aug 4, 2026
1b4704b
Contract: record feature 007's additive endpoints (frozen file, own c…
jeremymanning Aug 4, 2026
8a8f38a
The swap control -- and a theorem retracting one of my own spec claims
jeremymanning Aug 4, 2026
1d09552
The pretrained arm: what a word's form is actually worth
jeremymanning Aug 4, 2026
ce53c9f
Enable the swap control, and document the whole transform
jeremymanning Aug 4, 2026
904921a
Notes: CI's HuggingFace rate limiting, and the two durable fixes
jeremymanning Aug 4, 2026
ea9cd56
Actually use the HF_TOKEN secret: no workflow referenced it
jeremymanning Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@ concurrency:

env:
HF_HUB_DISABLE_TELEMETRY: "1"
# Authenticates every Hub call in this workflow. `huggingface_hub` and
# `transformers` both read HF_TOKEN from the environment, so nothing in the code
# needs to know about it.
#
# Why it is here: unauthenticated CI gets rate-limited hard, and three builds
# across two sessions died on 429s. The failure is not cosmetic — when
# `model_info` cannot resolve a revision to a commit sha, `export_static_assets.py`
# REFUSES to publish rather than emit weight URLs pinned to a moving `main`
# (issue #5). That refusal is correct, so the fix belongs here rather than in a
# looser guard.
#
# The secret is optional by design: forks and PRs from forks get an empty string,
# which `huggingface_hub` treats as anonymous — exactly today's behaviour, with
# today's retries. Nothing breaks without it; it is only more likely to succeed.
HF_TOKEN: ${{ secrets.HF_TOKEN }}

jobs:
backend:
Expand Down Expand Up @@ -162,8 +177,19 @@ jobs:
run: |
sh ../../scripts/npm-ci-retry.sh
npx playwright install --with-deps chromium
- name: e2e (both projects — chromium against the real backend, static against the built Pages bundle)
- name: e2e (all three projects — chromium against the real backend, static against the built Pages bundle, webgpu against a real adapter when the runner has one)
working-directory: code/frontend
# KNOWN GAP, stated so a green tick is not read as more than it is: the
# `webgpu` project needs a WebGPU adapter, and GitHub-hosted runners have no
# GPU (the software adapter they can offer advertises no shader-f16). It
# therefore SKIPS here, printing why, instead of passing vacuously on the WASM
# rung — so THIS JOB DOES NOT VERIFY THE WEBGPU PATH. It is verified by running
# `npx playwright test --project webgpu` on a machine with a real GPU.
# What this job does always verify about that defect: the non-degeneracy
# invariant and the dtype ladder (tests/unit/logitsSanity.test.ts, in the
# frontend job) and a real session passing the same load-time gate on the WASM
# rung (tests/e2e/static.spec.ts). Do not "fix" the skip by deleting the
# project — the absence of this coverage is what let webgpu/q4f16 ship.
run: npm run test:e2e
- name: Upload traces on failure
if: failure()
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ concurrency:

env:
HF_HUB_DISABLE_TELEMETRY: "1"
# Read by `huggingface_hub` and `transformers` directly; no code change needed.
# This job is the one that most needs it: it runs the REAL backend as a build tool
# and resolves every curated model's revision to a commit sha. On a 429 that
# resolution fails, and `export_static_assets.py` refuses to publish rather than
# ship weight URLs pointing at a moving `main` (issue #5) — so an unauthenticated
# rate limit does not degrade the deploy, it blocks it. Optional: absent, the
# value is empty and the Hub is called anonymously, exactly as before.
HF_TOKEN: ${{ secrets.HF_TOKEN }}

jobs:
build-and-deploy:
Expand Down
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"feature_directory":"specs/006-lexicon-lab-tiny"}
{"feature_directory":"specs/007-vacancy-transform-field"}
59 changes: 59 additions & 0 deletions code/backend/src/llm_geometry/api/routes_arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from pydantic import BaseModel

from ..arch import build_graph, check_model_size, generate, trace_forward, weight_window
from ..arch.vacancy_score import vacancy_score
from ..config import ARCH_DEFAULT_MAX_CONTEXT, ARCH_WEIGHTS_MAX_CELLS
from ..errors import InvalidParamError
from .encoding import jsonable_6sig

router = APIRouter(prefix="/arch", tags=["arch"])
Expand Down Expand Up @@ -57,6 +59,27 @@ class GenerateBody(BaseModel):
seed: int | None = None


class VacancyScoreBody(BaseModel):
"""`POST /api/arch/vacancy-score` request body (feature 007, contract §8).

`passages` defaults to the six evenly spaced excerpts of the shipped corpus the
measurement of §8.3a used, so an empty request reproduces the reference configuration.
`passage` is the singular sugar the panel uses when a reader edits one excerpt.

The transform's other knobs are deliberately absent: `consistent` and `reveal_after`
are fixed at the theorem's condition, because the number this endpoint produces is
only interpretable beside the tiny arm's exact zero, and that zero holds only there.
"""

model_id: str
passage: str | None = None
passages: list[str] | None = None
p: float = 1.0
seed: int = 0
match_prosody: bool = True
keep: list[str] = []


@router.get("/graph")
def arch_graph(model_id: str) -> dict[str, Any]:
"""The cached traced architecture graph (nodes/edges/meta/schema_version)."""
Expand Down Expand Up @@ -107,3 +130,39 @@ def arch_generate(body: GenerateBody) -> dict[str, Any]:
seed=body.seed,
)
)


@router.post("/vacancy-score")
def arch_vacancy_score(body: VacancyScoreBody) -> dict[str, Any]:
"""What a word's FORM is worth to a model that has one for it (contract §8).

Three variants of each passage — English, a real-word swap, and nonce — are scored by
three real forward passes each, and the mean NLL over the tokens of the words that
survive all three is reported per variant, together with the two differences that
decompose the damage: `nll(swap) − nll(english)` is the cost of **wrong content**,
`nll(nonce) − nll(swap)` the cost of **unknown form**. `nll(nonce) − nll(english)`
is returned but flagged `headline: false` — it is their sum and conflates them.

This is the full stack, at float32, so it reports everything. The static build runs a
quantized export and may report only what the measurement of §8.3a bounded for that
dtype; it refuses the rest by name rather than inventing an error bar.
"""
_gate(body.model_id)
if body.passage is not None and body.passages is not None:
raise InvalidParamError(
"send either `passage` (one) or `passages` (several), not both — they would "
"silently disagree about what was scored"
)
passages = body.passages
if body.passage is not None:
passages = [body.passage]
return _sig6(
vacancy_score(
body.model_id,
passages,
p=body.p,
seed=body.seed,
match_prosody=body.match_prosody,
keep=frozenset(body.keep),
)
)
Loading
Loading