Skip to content

Port Trust Hardening: quarantine synthetic facts and fail closed - #56

Merged
FHoffarth merged 5 commits into
developfrom
fix/port-trust-hardening
Aug 24, 2026
Merged

FHoffarth merged 5 commits into
developfrom
fix/port-trust-hardening

Conversation

@FHoffarth

Copy link
Copy Markdown
Owner

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:

  • quarantines unsupported synthetic port identity facts
  • retires the generators that created them
  • removes blanket field:"all" / generic OFFICIAL provenance
  • prevents the frontend bridge from originating passenger facts
  • removes synthetic passenger-facing defaults and fake source authority
  • ensures UNKNOWN stays UNKNOWN in the rendered UI
  • removes unsourced gangway, walking, accessibility, emergency, police, terminal and transit assertions from the live passenger surface
  • makes compiler tests side-effect-free
  • adds a Windows-safe compiler sandbox fallback without skipping tests

Trust principle

Evidence may be missing.

Missing evidence must remain UNKNOWN.

No downstream layer may silently manufacture canonical or passenger-facing truth.

Commits

# Commit Change
1 c3059ae chore(ports): quarantine synthetic port identity layer (ADR-0006)
2 5b7152f chore(ports): retire synthetic port generators, make compiler tests side-effect-free
3 55c2059 fix(bridge): stop the frontend bridge originating passenger-facing facts
4 588611b fix(ports): keep unknown values unknown on the passenger surface
5 e5e35ed test: make the compiler sandbox work without symlink privilege

Validation

  • backend: 944 passed, 0 failed, 0 errors
  • frontend: 113 passed
  • typecheck: no new errors vs baseline (400 pre-existing, delta zero)
  • production build: green
  • git diff --check: clean
  • data/ unchanged by tests
  • knowledge/ unchanged by tests
  • package-lock.json unchanged
  • both retired generators verified to fail before any writes — executed, not just read

Verified outcomes

Port identity quarantine — all 119 identity.json files parse; timezone null across all 119; zero non-null occurrences of walking_time_min, distance_to_city_center_m, gangway_deck_default, step_free_access, card_acceptance_pct, emergency_phone, negative_intelligence; berths: [], sources: []; zero blanket field:"all" records. Entity identity (slug, name, un_locode, country, region, coordinates) retained.

Frontend bridge — no hardcoded walkingTimeMin (now read from the knowledge layer, typed number | null); no timonelo.com authority; no OFFICIAL default; no fallback UN/LOCODE. The one remaining ITGOA in 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 develop the test suite rewrote two tracked data/ artifacts on every run, which destroyed the evidence of their drift and made git status after 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:

  • Port/Terminal/Berth ontology and K5 legacy terminal placeholders
  • duplicate entity pairs
  • UNECE/UNLOCODE validation
  • richer port provenance alignment
  • stale generated database artifact
  • remaining editorial PortBus statement in Barcelona

🤖 Generated with Claude Code

claude and others added 5 commits August 25, 2026 00:02
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>
@FHoffarth
FHoffarth merged commit 5311281 into develop Aug 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants