Step 1 of the control-marking task, and deliberately server-only: no handle is drawn yet. This project already shipped a range handle for `profile.interval`, which compileGeometry ignores, so the control dragged and changed nothing. The consumer comes first now. Two kinds join the taxonomy — `junction-crosswalk-inset` and `junction-stop-line-offset`, both on the existing `junction-approach` anchor. The solver writes them onto the approach entry, `applyDirectJunctionPlans` carries them onto the compiled approach, and `compileControlMarkings` reads them in place of the module constants it used for every junction. They move markings without reshaping the junction, so unlike width and cutback they deliberately do not trigger a boundary recompute. `applyJunctionConstraint` becomes an explicit switch. Its trailing `else` had meant every kind that was not approach-width fell through to the cutback validator, so a new kind would have been silently validated and written as a cutback. The same non-exhaustive shape in the test fixture's `valueFor` is fixed the same way, and now throws for an unnamed kind rather than answering with a corner radius. design.md's taxonomy is updated with it — a test asserts the two cannot drift, which is what caught the omission. Measured on a 41-road workspace with 8 crossings: both constraints change their marking geometry, neither drags the other, and out-of-range blocks instead of clamping. That measurement is not in the suite: the synthetic junction resolves `junction_inset_m` to 0 because its crossing never binds to a plan, and the committed OSM fixture has no crossings at all. The tests assert the wiring the handles will depend on — values reaching the approach entry, distinct branches, blocking diagnostics — and the gap is recorded in the test itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
62 lines
1.6 KiB
Python
Executable File
62 lines
1.6 KiB
Python
Executable File
"""
|
|
JSON file I/O utilities.
|
|
|
|
Provides read_json and write_json as the single source of truth
|
|
for JSON file operations across all Trellis scripts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
|
|
def read_json(path: Path) -> dict | None:
|
|
"""Read and parse a JSON file.
|
|
|
|
Returns None if the file doesn't exist, is invalid JSON, or can't be read.
|
|
"""
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
return None
|
|
|
|
|
|
def write_json(path: Path, data: dict) -> bool:
|
|
"""Write dict to JSON file with pretty formatting.
|
|
|
|
The write is atomic: content goes to a temp file in the same directory
|
|
and is then renamed over the target. A crash or Ctrl-C mid-write leaves
|
|
the existing file intact rather than truncated, so a corrupted task.json
|
|
can never make a task silently vanish from `task.py list`.
|
|
|
|
Returns True on success, False on error.
|
|
"""
|
|
payload = json.dumps(data, indent=2, ensure_ascii=False)
|
|
try:
|
|
fd, tmp = tempfile.mkstemp(
|
|
dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp"
|
|
)
|
|
except OSError:
|
|
return False
|
|
|
|
try:
|
|
try:
|
|
f = os.fdopen(fd, "w", encoding="utf-8")
|
|
except OSError:
|
|
# fdopen never took ownership of fd; close it ourselves.
|
|
os.close(fd)
|
|
raise
|
|
with f:
|
|
f.write(payload)
|
|
os.replace(tmp, path)
|
|
return True
|
|
except OSError:
|
|
try:
|
|
os.unlink(tmp)
|
|
except OSError:
|
|
pass
|
|
return False
|