Port Trust Hardening: quarantine synthetic facts and fail closed - #56
Merged
Merged
Conversation
All 119 knowledge/ports/*/identity.json files were emitted by two bulk
generators (tools/mass_populate_knowledge.py, 101 ports;
tools/populate_classes_and_ports.py, 18 ports) that hardcode template
constants. Corpus-wide, timezone is "UTC" 119/119, walking_time_min is
10 119/119, card_acceptance_pct is 98 119/119, and every port carries an
identical blanket source record:
{"field": "all", "source_id": "src:official-port-authority",
"trust_level": "OFFICIAL", "retrieved_at": "2026-08-16T12:00:00Z"}
field:"all" attests no field in particular; src:official-port-authority
resolves to no artifact; and OFFICIAL was applied by a generator to values
it invented. The defect is invisible per-file and only appears in the
distribution across the corpus.
Quarantines the synthetic fields to their fail-closed state (null scalars,
empty lists) and removes the blanket source records. Keys are retained
rather than deleted so the data records that a claim was asserted and
withdrawn. Entity identity fields are preserved for referential integrity
and explicitly NOT promoted to trusted facts; no provenance is invented
for them.
Extends scope to logistics.emergency_phone and logistics.currency, which
were not in the original plan. emergency_phone is a generator else-branch
yielding 112 for Mexico, Barbados, Cayman Islands and Israel -- all
incorrect, all published under OFFICIAL.
Enforcement is 13 corpus-wide, distribution-based invariants; a per-file
assertion cannot distinguish a researched 10 from a template 10.
Not fixed here: tools/generate_frontend_bridge.py hardcodes the same
values independently, so ports.ts stays contaminated until the frontend
contract is changed. data/ is excluded because it is already stale on
develop independently of this change. Both are documented in the audit.
Refs: docs/PORT_IDENTITY_CORPUS_AUDIT_2026-08-24.md
…ide-effect-free Follow-up to ADR-0006. Three changes. 1. Both proven generators fail closed on the port path. tools/mass_populate_knowledge.py (101 ports) and tools/populate_classes_and_ports.py (18 ports) could still recreate every quarantined value. Their port-identity construction is removed and replaced with refuse_port_population(), which raises with an explanation and a pointer to the ADR. The guard sits at the top of each entry function, before any write. Placed at the port section instead, a run rewrote 45 cruise-line and ship-class files before refusing -- a partial mutation of tracked knowledge by a script that cannot complete. The template literals are deleted as well as guarded, so the generators cannot reproduce the fields even if someone removes the guard as obstruction. PORTS_EXPANSION and ADDITIONAL_PORTS are kept: they record where the slugs, names, coordinates and LOCODEs entered the repository, and remain unvalidated candidate identity. 2. Compiler tests no longer mutate the working tree. compile() writes data/cruise_intelligence_db.json and data/cruise_knowledge_graph.json unconditionally and exposes no output-path option, so four tests rewrote two tracked files on every run. Since those artifacts are already stale on develop, this destroyed the evidence of that drift and made git status after a test run meaningless. tests/compiler_sandbox.py supplies a temporary root whose knowledge/ is a symlink to the real corpus: real data in, artifacts out to the sandbox. Nothing is restored afterwards, because nothing is modified -- restoring tracked files after mutating them is still mutation. 3. Four new corpus invariants. step_free_access uniformity and unsourced true claims; slug-interpolated berth names; uniform berth counts; uniform populated terminal shape. Plus behavioural and source-level checks that the generators stay retired. Accessibility gets its own pair rather than joining the numeric checks: a wrong walking time costs ten minutes, a wrong step-free claim sends a wheelchair user to a gangway they cannot board. Known residue, not fixed here: all 119 ports still declare exactly one name-only terminal. Emptying the array would drop 119 Terminal nodes and their LOCATED_ON edges from the graph, so terminal cardinality needs a referential integrity decision rather than a bulk edit. Audit K5; documented in the test. Tests 890 -> 903. data/ and knowledge/ unmodified by the full suite. Refs: docs/adr/ADR-0006.md
Audit U-1. tools/generate_frontend_bridge.py did not read passenger-facing port values from the knowledge layer -- it hardcoded them at generation time. Quarantining knowledge/ports/ therefore left the passenger surface unchanged, because the fabrication happened downstream of the layer being governed. Removed from the bridge: berths [Berth 1, Berth 2], gangwayDeckDefault=5, distanceToCenterM=500, walkingTimeMin=10, stepFreeAccess=True, cardAcceptancePct=98, currency and emergencyPhone from a country allow-list, blanket transitNote/airportTransit accessibility claims, a two-entry negativeIntelligence template, and a callingShips list naming three MSC vessels for every port on earth. Also removed: officialSource, which named Timonelo's own domain as the port authority under trustLevel OFFICIAL, and the p.get(un_locode, ITGOA) fallback that silently relabelled any codeless port as Genoa. The bridge now propagates provenance and never originates it. A source record survives to the frontend only if the knowledge layer carries one scoped to a named field; field:all is not provenance (ADR-0006 D2). Everything else is null. Regenerated ports.ts: 119 ports, 0 non-null for every fabricated field, the one remaining ITGOA being Genoa's own record. Contract changes, kept as narrow as the nulls require. PortData and CuratedPort fields become nullable. getPortBySlug returns undefined for an unknown slug rather than PORTS_REGISTRY[0], which presented one port's data under another port's name. Consumers fixed rather than data restored. frontend/src/ports.ts dropped its own fallbacks -- slug.toUpperCase() rendered YOKOHAMA in the UN/LOCODE field for a port absent from the knowledge layer, and Europe / Global, EUR and 98 were presentation defaults shaped like facts. PortExplorer renders unknown / unbekannt where a value is missing, and no longer appends (step-free) to the walking-time line: stepFreeAccess is null for every port, so the claim had nothing behind it. Not included: frontend/src/generated/database.json. Regenerating it pulls in the pre-existing msc-bellissima and msc-meraviglia drift documented as U-2, and nothing in the frontend imports it. Backend 903 -> 922. Frontend 91 passed, typecheck 400 errors unchanged from baseline, production build green. data/ and knowledge/ untouched. Refs: docs/PORT_IDENTITY_CORPUS_AUDIT_2026-08-24.md
Audit U-1 follow-up. 1. Walking time is now three-state. `(walkingTimeMin ?? 0) > 0` collapsed null and 0 into one branch and answered both with "Shuttle transfer or taxi recommended". Not knowing how far the centre is does not imply a shuttle exists, that a taxi is warranted, or that walking is impractical. Unknown now renders as "Walking time unknown" / "Gehzeit unbekannt". 0 is treated as unknown too. It appears once in the data (Shanghai, 24 km) as an informal sentinel for "too far to walk", but that meaning is documented nowhere and is indistinguishable at the render site from a missing value. 2. CURATED_PORT_STORIES no longer supplies operational facts. The block held hand-written passenger facts for six ports with no provenance model. Descriptive framing stays; anything that changes what a passenger does is wired to null: walkingMin, distanceKm, gangwayDeck, terminalPier, emergency, police, transit and airport notes, essentials. The literals stay in the source block as candidates for a future sourced pipeline rather than being deleted. 3. Four story texts asserted the same facts in prose. Neutralising the structured fields while the narrative still read "From Gangway Deck 5, the city centre is accessible in a 15-minute walk" and "offers completely step-free access" would have been cosmetic. Genoa (deck, walking time), Yokohama (step-free, walking distance) and Shanghai (berth assignment) lost those clauses and kept their framing. Render sites guarded so absent values produce an explicit statement rather than an empty panel: terminal pier, emergency numbers, transit cards, and the LOCODE parenthesis that rendered as "Yokohama ()". Not addressed: Barcelona's story describes the PortBus shuttle service, which is a transit claim without evidence. It reads as geographic framing rather than advice, and deleting it shades into rewriting editorial copy, so it is reported rather than changed. Frontend 91 -> 113. Backend 922 unchanged. Typecheck 400, identical to baseline. Build green. data/, knowledge/ and package-lock.json untouched. Refs: docs/PORT_IDENTITY_CORPUS_AUDIT_2026-08-24.md
The sandbox added alongside the port quarantine exposed the real corpus to KnowledgeDBCompiler via a symlink. That is not portable: on Windows os.symlink needs SeCreateSymbolicLinkPrivilege, which an ordinary account without Developer Mode does not hold, so the call fails with WinError 1314 and takes 13 tests with it -- 5 in test_cruise_intelligence_db, 4 in test_live_fleet_operations, 3 in test_source_network, and the compiler tolerance test that inlined the same call. CI runs on ubuntu-latest and would not have caught it. expose_knowledge() now links where it can and copies the corpus where it cannot, and both call sites use it. Copying rather than skipping is the point: these tests exist to exercise the real corpus, and skipping them on the platform this repository is developed on would drop the coverage exactly where a regression shows up first. The corpus is ~2 MB across ~450 files, so the copy costs little. FileExistsError is deliberately not caught -- an already-populated root is a caller bug, not a platform limitation. The compiler only reads the corpus, so knowledge/ is untouched in both modes and the guarantee the sandbox exists for is unchanged: no test writes into data/ or knowledge/. Backend goes from 931 passed / 6 failed / 7 errors to 944 passed on Windows. No production code is touched, and the reviewed 0001-0004 series is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This change hardens Timonelo's port intelligence trust boundary across the full pipeline from knowledge files to passenger-facing UI.
The original audit proved that repeated port facts were template-generated rather than independently sourced.
This PR:
field:"all"/ generic OFFICIAL provenanceTrust principle
Evidence may be missing.
Missing evidence must remain UNKNOWN.
No downstream layer may silently manufacture canonical or passenger-facing truth.
Commits
c3059ae5b7152f55c2059588611be5e35edValidation
git diff --check: cleandata/unchanged by testsknowledge/unchanged by testspackage-lock.jsonunchangedVerified outcomes
Port identity quarantine — all 119
identity.jsonfiles parse;timezonenull across all 119; zero non-null occurrences ofwalking_time_min,distance_to_city_center_m,gangway_deck_default,step_free_access,card_acceptance_pct,emergency_phone,negative_intelligence;berths: [],sources: []; zero blanketfield:"all"records. Entity identity (slug, name, un_locode, country, region, coordinates) retained.Frontend bridge — no hardcoded
walkingTimeMin(now read from the knowledge layer, typednumber | null); notimonelo.comauthority; noOFFICIALdefault; no fallback UN/LOCODE. The one remainingITGOAin the generated bridge is the Genoa record sourced from its own identity file, where it is the genuine code.Passenger surface — unknown walking time renders as unknown (0 treated as unknown too); null implies no shuttle or taxi recommendation; no unconditional step-free claim; no slug-derived UN/LOCODE; curated unsourced operational facts wired to null with editorial copy retained.
Side benefit
On
developthe test suite rewrote two trackeddata/artifacts on every run, which destroyed the evidence of their drift and madegit statusafter a test run meaningless. Commit 2 ends that; commit 5 makes it work on Windows too.Important intentional behavior
The passenger port surface is now intentionally sparse where evidence does not exist.
This is expected and preferred over unsupported precision.
Known follow-ups / out of scope
Not regressions — pre-existing conditions this PR does not attempt to resolve:
🤖 Generated with Claude Code