Skip to content

Commit df48d8f

Browse files
committed
[feat.core.Omega.D8+] PR-05: the same fields.qm.homo now runs through a transport-free kernel and an HTTP route and produces the SAME artifact digest
THE ACCEPTANCE TEST THE AUDIT NAMED FIRST now runs as gate 13, and it passes on the two transports that exist: core (in-process InvocationService) vs HTTP /field — 19 of 19 facts identical cube sha256 06b686f0d90258ea147493718651c0428e11186e0c7c18de5eddb053ffa520d9 6,746,050 bytes · 7 basis functions · E = -74.958608271 Ha · version c6bc04cc54f4 python_sdk / cli_json / http_v2 / mcp: reported ABSENT, never as passing WHAT LANDED, and the ONE design decision that carries it: a handler is a STRING (`module:function`) in the descriptor, resolved by importlib at invocation time and never at load time. So the catalog can list 10 methods, validate a caller's payload against the canonical schema, explain a refusal and estimate a cost with RDKit NOT IMPORTED — measured: `rdkit in sys.modules` is False after loading the full catalog. That is the property that makes `dirac methods list` possible on a laptop with no chemistry stack, and it is why the CLI can be a client rather than a remote control. catalog.py descriptors -> MethodSpec (contract + handler ref + cost model) invocation.py invoke() -> v2 envelope. Store/ledger/cache are INJECTED, so the offline case is the same code path, not a degraded one handlers.py the only layer allowed to import RDKit/pyscf cube.py canonicalisation — see below, it is the real finding THE REAL FINDING, and the acceptance test is what produced it — no amount of reading would have: PYSCF WRITES THE WALL CLOCK INTO EVERY CUBE'S SECOND LINE. three identical runs of the identical calculation: raw digests 7d30b3c3… / 9986e64a… / cd984ebe… all different line 1 replaced fcf18034… / fcf18034… / fcf18034… identical So the SHA-256 of a quantum field was a function of the time of day. Consequences in the order they matter: content-addressed storage could never DEDUPLICATE a quantum cube (PR-04's proven idempotence was unreachable in production for six of ten methods); the cross-transport digest test was IMPOSSIBLE rather than unproven; and it passed once by luck, when both legs landed in the same wall-clock second — green when the machine is fast, red when it is loaded, and the red reads as a real regression. Fixed in the CONSUMERS (route + handler + cache read), not inside field_quantum, because that function's source is hashed into the method version and a comment line is not worth invalidating every cached quantum field for — a cost paid once already today and measured. The timestamp is not discarded: it moves to meta.toolkit_wrote_at, because it is a fact about the REQUEST and the bytes must be a function of the INPUT. Measured after: two independent computations 2 seconds apart -> one digest, ONE blob row, 6.7 MB instead of 13.5 MB. AND THE SAME TEST FOUND A SECOND HOLE IT HAD BEEN HIDING: `version` was None on BOTH legs, which the diff scored as agreement. That is a check that cannot fail, on the very field the audit lists first. There is now a MUST_BE_PRESENT set: any compared fact absent on both sides is reported as VACUOUS AGREEMENT and fails, rather than passing for lack of content. Fixing it exposed a third: my own new meta key made normalize_meta raise, the route's lenient fallback served un-normalised meta, and that silently removed the version stamp — a fallback that skips a whole step also skips the step's other effects. The log said so twice and nobody was reading the log. SCRATCH MOVED OFF THE tmpfs, in this commit because the parity test could not run without it. pyscf's cubegen writes the whole 6.7 MB cube into TMPDIR before returning it, and /tmp here is a 30 GB tmpfs with a PER-USER quota (read out of /proc/mounts). NEGATIVE CONTROL, which is what makes this a measurement: with TMPDIR=/tmp the identical calculation dies at `OSError: [Errno 122] Disk quota exceeded` inside cubegen.write; pointed at disk it completes with 425 GB free. Redirected in CODE (backend/scratch.py, imported before pyscf) so every entry point is covered — a unit-file variable would have fixed the daemon and left every test, script and hand-run pointed at the quota. The unit files set it too, so an operator can see it. check_scratch_headroom.py measures the number that actually ran out (per-USER occupancy, not df — df reported 6 GB free throughout the outage, because df measures the filesystem and the limit was on the user). INSTRUMENT DEFECTS FIXED, both mine: the contract-drift extractor scanned for `meta[...]` assignments and could not see the normalize wrapper's `out[...]`, so it reported a real key as "declared but never emitted". Widened, with a proxy-ledger note naming the unsound direction — the alternative was renaming a backend variable to suit the instrument. Verification: 14/14 gates, including the new gate 13 and the v1 golden (re-captured deliberately — v1 gained two meta keys, additive, and the reason is above). 34 new tests across three suites. Portability ratchet 6 -> 7 of 13. NEXT: PR-06 wires the route itself to InvocationService so there is one orchestration instead of two agreeing ones — and the parity test is what makes deleting the old one a measurement rather than an act of faith.
1 parent 8a70942 commit df48d8f

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

‎scripts/check_contract_drift.mjs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,25 @@ for (const m of fieldMetaSource.matchAll(/\bmeta\[\s*['"](\w+)['"]\s*\]\s*=/g))
337337
backendFieldMetaKeys.add(m[1]);
338338
}
339339

340+
// (d) the normalize_meta WRAPPER writes into a dict it calls `out`, not `meta`, and the
341+
// scan above could not see it. Found the honest way: the gate reported
342+
// `iface.pyi declares ["method_version"], which the backend extraction did not find
343+
// anywhere — either a dead contract key or a broken extractor`, and it was the
344+
// extractor. Widened rather than working around it, because the alternative was to
345+
// rename a variable in the backend to suit an instrument, which is fitting the
346+
// object to the measurement.
347+
//
348+
// PROXY LEDGER. PROPERTY: every key the backend can emit. PROXY: bracket
349+
// assignments into a dict named `meta` OR `out` within the excised region.
350+
// IMPLICATION: proxy => property (an assignment IS an emission); property => proxy
351+
// FAILS for any third variable name. SAFE SIDE: a key the scan FINDS is really
352+
// emitted, so `missing from iface.pyi` is sound; a key it does NOT find may still be
353+
// emitted, which is why the `declares X the backend does not emit` branch says
354+
// "check by hand" instead of failing outright.
355+
for (const m of fieldMetaSource.matchAll(/\bout\[\s*['"](\w+)['"]\s*\]\s*=/g)) {
356+
backendFieldMetaKeys.add(m[1]);
357+
}
358+
340359
if (backendFieldMetaKeys.size < 10) {
341360
fail(`static analysis found only ${backendFieldMetaKeys.size} FieldMeta key(s) in `
342361
+ `${backendPath} — the extraction is almost certainly broken, not the backend `

‎scripts/check_scratch_headroom.py‎

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
"""Is there room to write a cube? Asked before it matters, not after.
3+
4+
THE FAILURE THIS EXISTS TO PRE-EMPT, measured 2026-08-11: /tmp on this box is a 30 GB
5+
tmpfs mounted with `usrquota`. User ivan reached 23.5 GB across 313,374 files, and the
6+
next write returned EDQUOT. What that looked like from inside the session:
7+
8+
· EVERY shell command exited 1, because the tool harness writes command output to /tmp
9+
· `df` reported 6.0 GB free throughout — df measures the FILESYSTEM, the limit was on
10+
the USER, and the two numbers have nothing to do with each other
11+
· pyscf died inside cubegen.write with `OSError: [Errno 122] Disk quota exceeded`,
12+
which reaches a chemist as an internal error about a path they have never heard of
13+
14+
The durable fix is backend/scratch.py, which sends all scratch to a 1.8 T ext4 volume.
15+
This script is the SECOND half: the fix only holds while nothing points back at /tmp,
16+
and a check that watches the wrong number is worse than none. So it measures BOTH:
17+
18+
① where the scratch is configured to go, and whether that is off the tmpfs
19+
② the per-user occupancy of /tmp, which is the number that actually ran out
20+
21+
WHY IT IS A RATCHET AND NOT A FIXED LIMIT: the quota itself is unreadable here — the
22+
`quota` binary is not installed — so the threshold cannot be derived from the system. It
23+
is derived from the OBSERVED wedge (23.5 GB) with a margin, and the number is written
24+
down as a measurement with its date rather than chosen as a round figure.
25+
26+
Usage: python3 scripts/check_scratch_headroom.py [--selftest]
27+
Exit: 0 healthy · 1 scratch is on the quota-limited tmpfs, or occupancy is near the
28+
observed wedge point · 2 could not measure
29+
"""
30+
from __future__ import annotations
31+
32+
import os
33+
import pathlib
34+
import shutil
35+
import sys
36+
37+
ROOT = pathlib.Path(__file__).resolve().parent.parent
38+
sys.path.insert(0, str(ROOT / 'backend'))
39+
40+
# Measured 2026-08-11: the write that failed did so with 23.5 GB held by this user.
41+
OBSERVED_WEDGE_GB = 23.5
42+
# Warn with enough room left to finish a round of work rather than at the cliff edge.
43+
WARN_AT_GB = 18.0
44+
45+
46+
def tmpfs_mounts() -> dict[str, dict]:
47+
"""Which mounts are tmpfs, and which of those enforce a per-user quota.
48+
49+
Read out of /proc/mounts rather than assumed, because `usrquota` is the entire
50+
reason df was misleading, and a future remount without it would make this check
51+
warn about a limit that no longer exists.
52+
"""
53+
out = {}
54+
try:
55+
for line in pathlib.Path('/proc/mounts').read_text().splitlines():
56+
parts = line.split()
57+
if len(parts) >= 4 and parts[2] == 'tmpfs':
58+
opts = parts[3].split(',')
59+
out[parts[1]] = {
60+
'usrquota': 'usrquota' in opts,
61+
'size': next((o.split('=')[1] for o in opts
62+
if o.startswith('size=')), 'unset')}
63+
except OSError:
64+
pass
65+
return out
66+
67+
68+
def user_bytes_in(path: str) -> tuple[int, int]:
69+
"""Bytes and file count owned by this user under `path`, one filesystem only.
70+
71+
os.walk with a uid filter rather than `du`: the number that matters is per-USER, and
72+
du would happily total another user's files and report a healthy figure while this
73+
user's quota is exhausted. That is the same class of mistake as reading df.
74+
"""
75+
uid = os.getuid()
76+
total = files = 0
77+
root_dev = None
78+
for dirpath, dirnames, filenames in os.walk(path, onerror=lambda e: None):
79+
try:
80+
st = os.stat(dirpath)
81+
except OSError:
82+
continue
83+
if root_dev is None:
84+
root_dev = st.st_dev
85+
elif st.st_dev != root_dev:
86+
dirnames[:] = []
87+
continue
88+
for name in filenames:
89+
try:
90+
fst = os.lstat(os.path.join(dirpath, name))
91+
except OSError:
92+
continue
93+
if fst.st_uid == uid:
94+
total += fst.st_size
95+
files += 1
96+
return total, files
97+
98+
99+
def main() -> int:
100+
problems: list[str] = []
101+
notes: list[str] = []
102+
103+
mounts = tmpfs_mounts()
104+
quota_mounts = [m for m, info in mounts.items() if info['usrquota']]
105+
notes.append(f'tmpfs mounts with a per-user quota: {quota_mounts or "none"}')
106+
107+
# ── ① where is scratch configured to go? ─────────────────────────────────
108+
try:
109+
import scratch
110+
target = str(scratch.scratch_dir())
111+
except Exception as e: # noqa: BLE001
112+
print(f'check_scratch_headroom: cannot import backend/scratch.py ({e}); the '
113+
f'redirection cannot be verified and is therefore UNVERIFIED, not '
114+
f'healthy', file=sys.stderr)
115+
return 2
116+
117+
on_quota_fs = None
118+
for m in quota_mounts:
119+
if target == m or target.startswith(m.rstrip('/') + '/'):
120+
on_quota_fs = m
121+
if on_quota_fs:
122+
problems.append(
123+
f'scratch is configured at {target}, which is under {on_quota_fs} — a tmpfs '
124+
f'with a per-user quota. pyscf writes the whole cube there before returning '
125+
f'it, so a full quota kills every quantum request with an OSError from '
126+
f'inside cubegen. Set DIRAC_SCRATCH, or let backend/scratch.py default.')
127+
else:
128+
du = shutil.disk_usage(target if pathlib.Path(target).exists()
129+
else pathlib.Path.home())
130+
notes.append(f'scratch → {target} · {du.free / 1e9:.0f} GB free on its volume')
131+
if du.free < 5e9:
132+
problems.append(f'scratch volume has only {du.free / 1e9:.1f} GB free')
133+
134+
# ── ② the number that actually ran out ───────────────────────────────────
135+
for m in quota_mounts:
136+
b, n = user_bytes_in(m)
137+
gb = b / 1073741824
138+
pct = 100 * gb / OBSERVED_WEDGE_GB
139+
line = (f'{m}: this user holds {gb:.1f} GB in {n:,} files '
140+
f'({pct:.0f}% of the {OBSERVED_WEDGE_GB} GB that wedged the session on '
141+
f'2026-08-11)')
142+
if gb >= OBSERVED_WEDGE_GB:
143+
problems.append(line + ' — AT OR ABOVE the observed failure point')
144+
elif gb >= WARN_AT_GB:
145+
problems.append(line + f' — above the {WARN_AT_GB} GB warning line, and the '
146+
f'first symptom is every shell command exiting 1, '
147+
f'not a disk error')
148+
else:
149+
notes.append(line)
150+
151+
for n in notes:
152+
print(f' OK {n}')
153+
for p in problems:
154+
print(f' FAIL {p}')
155+
if problems:
156+
print('\nThe cheapest reclaim is stale per-session scratch directories; the '
157+
'durable fix is that nothing writes GB-scale files to a RAM-backed '
158+
'filesystem with a per-user quota in the first place.')
159+
return 1 if problems else 0
160+
161+
162+
def selftest() -> int:
163+
"""Prove the check can CONVICT, by pointing scratch at the quota-limited tmpfs.
164+
165+
Without this the healthy verdict is unfalsifiable — and a check whose only observed
166+
output is PASS has not been shown to have resolution.
167+
"""
168+
import subprocess
169+
env = dict(os.environ, DIRAC_SCRATCH='/tmp/dirac-selftest-scratch')
170+
r = subprocess.run([sys.executable, __file__], capture_output=True, text=True,
171+
env=env)
172+
convicted = r.returncode == 1 and 'per-user quota' in r.stdout
173+
print(r.stdout.strip())
174+
print('─' * 90)
175+
print(f'{"RED OK " if convicted else "BLIND "} pointing scratch at the tmpfs '
176+
f'makes the gate fail (exit {r.returncode})')
177+
return 0 if convicted else 1
178+
179+
180+
if __name__ == '__main__':
181+
sys.exit(selftest() if '--selftest' in sys.argv else main())

‎src/app.frontend.facets.molstar-rdkit.editable/facets/field-wells/index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ const Kinds: Record<FieldKind, KindSpec> = {
266266
type CacheSource = 'browser' | 'memory' | 'db' | 'computed';
267267

268268
interface FieldMeta {
269+
method_version?: string | null; // the running SOURCE digest of the method
270+
toolkit_wrote_at?: string | null; // pyscf stamps the wall clock INSIDE the cube;
271+
// moved into meta so the bytes stay a function
272+
// of the input (backend/cube.py)
269273
kind: string;
270274
basis?: string; // quantum kinds only
271275
method?: string;

0 commit comments

Comments
 (0)