Appendix D — Recorded defects

63 defects are on file against the surfaces this document describes. Each is a case of the software doing something other than what it says, recorded with a reproduction and with what is not established separated from what is.

This list is generated from docs/issues/, so it cannot fall behind the folder.

D.1 /api/ag-ui-chat decodes the JWT without verifying it, and a body flag skips it entirely

Severity high if reachable unauthenticated; the route is caller-facing by design · Status Verified — a forged token is rejected on dev (401); the admin_mode bypass is refused (400); a real token works (200)

docs/issues/20260812_agui_chat_jwt_never_verified.md

D.2 inspection_data returns whole image rows, so storage URLs reach the model and the stream

Severity high · Status Verified — fixed 2026-08-12 (PR pending), re-run 6/6 against the reproduction

docs/issues/20260812_kawa_inspection_data_leaks_storage_urls.md

D.3 six kawa tools query Supabase with the service-role key and no tenant filter

Severity low as deployed (not on the serving path); the code shape remains wrong · Status Fixed 2026-09-10

docs/issues/20260812_kawa_tools_query_without_tenant_scope.md

D.4 the service-role auth gate exempts /ag-ui-chat on a justification that is false

Severity medium — it is why the high-severity issue below went uncaught; no new exposure of its own · Status Verified — the exemption is gone, the route holds no service-role client, and on dev it rejects a forged caller (401)

docs/issues/20260812_service_role_gate_false_exemption.md

D.5 /api/ag-ui-chat is the last chat-surface route still on the service-role key

Severity medium — it writes as an unverified subject (see the coupled issue), through a service-role client that bypasses RLS · Status Verified — converted; on dev the route accepts a real caller (200) and rejects a forged one (401)

docs/issues/20260813_agui_chat_still_uses_service_role.md

D.6 the gas_readings policies authenticate the caller without authorizing the row

Severity high — any signed-in user of any organisation can read, edit and delete every organisation’s sensor readings by querying the table directly · Status Verified — scoped policies (#595) applied to the shared prod DB 2026-08-13; the four broad auth.role() policies are gone, gas_readings SELECT is survey-scoped (confirmed in-transaction on prod)

docs/issues/20260813_gas_readings_policy_authenticates_but_does_not_authorize.md

D.7 GET /api/organizations/get-members now returns 200-empty to a non-member, not 403

Severity low — a wire-behaviour change, no data exposure either way · Status Fixed 2026-08-25 — decided a defect, not by design: option (2), restore the 403. The handler asks is_org_member_secure before the list read, purely to choose the status code; the read still runs under RLS.

docs/issues/20260813_get_members_returns_empty_not_403.md

D.8 migrations do not reach the database automatically, and db push is blocked by history divergence

Severity medium — operational: merged schema/RLS changes are not live until someone applies them manually, and the standard tool for that is currently broken · Status Confirmed — ownership decided 2026-08-13 (KavApps is the single source of truth, ADR-009); remaining work is the transition

docs/issues/20260813_migrations_not_auto_applied_and_history_diverged.md

D.9 image analysis returns 404 because an ambiguous datasets embed is masked as “Image not found”

Severity high — the “analyze this image” feature is unusable from the web app: every attempt 404s · Status Fixed — root-caused locally and corrected in three routes; PR open

docs/issues/20260814_analyze_404_datasets_embed_ambiguity.md

D.10 human image anomalies have no home in the damage-mechanism catalog

Severity medium — real, on-asset findings cannot be recorded, so they are silently dropped · Status Fixed — typed findings (a finding is grounded in a mechanism OR an observation) landed 2026-08-14; migration + API #605, applied to prod

docs/issues/20260814_image_anomalies_do_not_map_to_the_mechanism_catalog.md

D.11 GET /datasets hands every member the storage account key

Severity — · Status Open — the rotation, and only the rotation. All three serving

docs/issues/20260817_dataset_credentials_returned_to_every_member.md

D.12 any authenticated user can rename an annotation category, rewriting other tenants’ image tags

Severity high — a cross-tenant write, available to the lowest role, through a supported API · Status Fixed 2026-08-18 — both halves. The role floor first (write policies require the editor role), then the tenancy: annotation_categories is now organization-scoped, the 17 shared rows are split, and no annotation or finding points at another organization’s category. Applied and reconciled on the shared database. Closed properly 2026-08-25: the trigger named in Affects was still selecting on annotations.category_id alone — see the note at the end.

docs/issues/20260818_any_user_can_rename_another_tenants_annotation_categories.md

D.13 deleting an annotation category would delete the findings grounded in it

Severity low as deployed, high the moment a delete is exposed — 4 of the 5 findings on the platform are observation-grounded and would go with their category · Status Fixed 2026-08-25 — supabase/migrations/20260825200000_findings_survive_a_deleted_class.sql changes edm_observation_category_id_fkey from ON DELETE CASCADE to ON DELETE RESTRICT, applied to the shared database and verified. The guarantee is now a schema property rather than a product decision. See the notes at the end.

docs/issues/20260818_deleting_an_annotation_category_deletes_its_findings.md

D.14 any signed-in user can read and write another organization’s gas survey readings

Severity high — a cross-tenant read of survey data, available to the lowest role, and reachable by an AI engine · Status Fixed 2026-08-18 — both policies dropped, so RLS denies every client path; the rows are preserved and the service role is unaffected. Applied and verified on the shared database.

docs/issues/20260818_gas_readings_staging_readable_by_every_tenant.md

D.15 KavApps kap-stable’s migrations reference helpers it never defines, so they cannot replay from empty

Severity medium — no live impact, but the set that ADR-009 calls the source of truth cannot rebuild the schema it is the source of truth for · Status Open — latent; nothing is broken while both repositories share one live database

docs/issues/20260818_kap_stable_migrations_cannot_rebuild_the_schema.md

D.16 five tables let a viewer write, pending a review of who runs ingestion

Severity medium — a viewer can add rows inside their own organization; not a cross-tenant leak and nothing destructive · Status Fixed 2026-08-19 — decided option A for four tables and option D for data_groups, shipped the same day by supabase/migrations/20260819120000_ingestion_writes_require_editor.sql and applied. The file was never re-statused; confirmed against the live database 2026-08-25. See the resolution at the end.

docs/issues/20260819_five_tables_let_viewers_write_pending_ingestion_review.md

D.17 any authenticated user can append to the trace log under any session id

Severity medium — write integrity on an audit log, not a data leak: nothing can be read or altered, only forged · Status Fixed 2026-08-25 — supabase/migrations/20260825180000_message_traces_record_the_caller.sql is applied to the shared database. The RPC now stamps written_by from auth.uid() and written_by_role from auth.role(), so a trace can no longer be appended anonymously under a forged session_id. Not Verified: the harmless reproduction below is still unexecuted, because its middle step is a write. See “Resolution” at the end.

docs/issues/20260819_message_traces_can_be_written_under_any_session_id.md

D.18 any signed-in user can overwrite or delete any other user’s avatar

Severity low — 4 objects, avatars only, no tenant data; but it is a write anyone can perform on anyone · Status Verified 2026-08-24 — fixed by 20260821000000_scope_avatar_writes_to_their_owner.sql

docs/issues/20260820_avatar_writes_are_open_to_every_signed_in_user.md

D.19 decision-log.test.ts fails 5 of 8 assertions on kap-stable

Severity — · Status Open — filed against KavApps kap-stable, not KAP

docs/issues/20260820_decision_log_test_fails_on_its_own_branch.md

D.20 27 handlers on kap-stable still authenticate inline

Severity — · Status Open — forward work (KAP → kap-stable), not a backport

docs/issues/20260820_twenty_seven_handlers_still_use_the_old_auth_idiom.md

D.21 /chat/agui/stream forwards a JWT it never verifies

Severity low — no data reachable without a valid token, but an anonymous caller · Status Fixed 2026-08-23 by 38e15685 — the one line below was applied,

docs/issues/20260821_agui_stream_forwards_a_token_it_never_verifies.md

D.22 GET /images accepts lat, lng and radius, echoes them back, and filters on none of them

Severity medium — no data is exposed that should not be, but a caller asking for images near a point receives every image and no indication that the filter was ignored · Status Fixed — GET /api/images filters through public.images_within_radius (PR #773). The migration was applied to the shared database on 2026-08-21 and verified there: SECURITY INVOKER holds (anon sees 0 rows through the function, the same 0 it sees on the table, against 12,328 for postgres), the radius sweep is monotonic over 27,221 geotagged images. Mirrored to KavApps kap-stable in Soterinc/KavApps#1148; the extern/KavApps pointer bump follows that merge

docs/issues/20260821_get_images_location_parameters_are_not_applied.md

D.23 the annotator rewrote swagger.yaml and deleted all 24 of its hand-written comments

Severity medium — no runtime effect; the loss is reasoning that took a lint run and a triage to establish, and its absence is silent · Status Fixed for the recurrence, not for the instance — annotate_signatures.py now round-trips with ruamel.yaml and refuses to write if the comment count drops (PR #773, decision B3). The 24 deleted lines are still deleted; restoring them is a separate call. Confirmed by diff: 24 present at 5983833, 0 at origin/main, deleted by f2baa8a

docs/issues/20260821_swagger_comments_deleted_by_yaml_rewrite.md

D.24 require_auth checks that a token is present, not that it is valid

Severity low as it stands, and the reason is not in this function — see “Why · Status Open — confirmed by test; no data exposure established

docs/issues/20260823_require_auth_does_not_validate_the_token_it_returns.md

D.25 a URL built with a function call inside it is invisible to the consumer scan

Severity — · Status Fixed 2026-08-25 in #833, folded in beside the two over-reporting defects. Interpolations are matched whole, so a ) inside ${ … } no longer ends the path; pinned by test_a_call_inside_the_url_does_not_truncate_the_path

docs/issues/20260825_a_url_built_with_a_nested_call_is_invisible.md

D.26 an intent told readers GET /gas-readings was not tenant-scoped, and it is

Severity low as a defect, higher as a document — it is the kind of sentence that starts an incident that is not happening · Status Fixed — the sentence is corrected. No behaviour changed, because nothing was wrong with the behaviour

docs/issues/20260825_an_intent_said_an_endpoint_was_unscoped_and_it_was_not.md

D.27 an RLS-bypassing create path, described as internal, is wired to a page

Severity — · Status Open — reported, not fixed

docs/issues/20260825_datasets_create_bypasses_rls_from_a_page.md

D.28 a write operation can be credited with callers that only read it

Severity — · Status Fixed 2026-08-25. The cause first recorded here was wrong — see

docs/issues/20260825_write_operations_credited_with_readers.md

D.29 components/chat/agui-chat-interface.tsx is not mounted anywhere

Severity — · Status Open — reported, not fixed

docs/issues/20260826_agui_chat_interface_has_no_consumer.md

D.30 krml_save_report writes pdf_reports rows nobody’s RLS can read

Severity — · Status Open — reported, not fixed. No evidence found that it is called by anything reachable from the deployed product

docs/issues/20260826_krml_save_report_writes_ownerless_unreadable_rows.md

D.31 a route comment says the findings table’s RLS predates shared workspaces — it does not, since 2026-08-04

Severity — · Status Open — reported, not fixed. Low severity: the route it is written on works correctly regardless

docs/issues/20260826_stale_comment_says_edm_rls_does_not_fit_shared_workspaces.md

D.32 three routes classify an image as thermal or RGB three different ways, and /api/images finds no thermal images at all

Severity medium · Status Fixed 2026-08-30 — see Resolution.

docs/issues/20260830_three_thermal_rgb_heuristics_disagree.md

D.33 any signed-in user can load any CAD model they have the URN for

Severity medium — requires a URN the caller was once given, so it is not reachable by guessing; but it survives losing organization membership, and the asset is a customer’s plant model · Status Confirmed — verified against kap-dev on 2026-08-31, not inferred from the code. Not fixed.

docs/issues/20260831_a_viewer_token_outlives_the_org_boundary.md

D.34 kap_cad_query’s unit_code silently matches nothing, because it means a different “unit”

Severity medium — no data exposure and a workaround exists, but the failure is silent and the query is safety-relevant · Status Fixed 2026-08-31 — unit_code removed from kap_cad_query, and equip_no documented as taking a full tag and substring-matching. Scoping decided per asset, so no anchored unit filter is built: the substring behaviour is now stated rather than a trap.

docs/issues/20260831_cad_query_unit_code_means_a_different_unit.md

D.35 candidate priority is a keyword match, and the keywords saturate

Severity low — nothing breaks, but a priority that never varies stops being a priority, on the exact campaign where triage most needs one · Status Open — filed per D5 of the proposal below; not fixed here, and not recommended to be fixed there. Fix direction decided: priority should be a decision (by a user or an AI), not a keyword match. Not yet built.

docs/issues/20260831_candidate_priority_is_always_critical.md

D.36 two CI gates report success while checking nothing

Severity high — not for a defect it has caused, but for the class it can no longer catch: x-service-role is a security claim published to callers, and nothing re-derives it from the handlers any more · Status Fixed 2026-09-01 — both gates read the composed spec and check 160 operations again; the extractor now follows reads into imported helpers, and a write verb only counts when it is chained off a table. 22 annotations across 19 operations were wrong and are corrected. The empty-set guard stays as the backstop.

docs/issues/20260901_annotation_gates_pass_over_zero_operations.md

D.37 an asset’s nearby-image count double-counts a photo two campaigns share

Severity low-medium — no data corruption and RLS is unaffected, but it silently inflates the exact signal kap_list_assets and the Assets register use to rank which asset needs attention most · Status Verified — re-ran kap_list_assets(organization_id: "<Polycarbon org id>") against kap-dev post-deploy (2026-09-01): T910-AP907 dropped from 329 to 314, exactly the real campaign’s own count predicted in “Verification” below.

docs/issues/20260901_asset_image_counts_double_count_shared_campaign_images.md

D.38 kap_cad_query’s “material” was a DGN drawing colour, not construction material

Severity low — no data exposure, but a value labeled “material” reached both AI tools and the real product UI with no source that could back it, on a page integrity engineers use for damage-mechanism judgment calls · Status Fixed, four instances of the same root problem — a “material” claim with no trustworthy source, at four different layers — found and fixed the same day: the CAD drawing-colour bug (migration + API/MCP, PRs #1078/#1080), CadDesignBlock on the asset detail page (PR #1089/#1092), pds_assets.material itself removed from the API/UI/MCP surface (same PRs), and finally the underlying database column cleared (migration 20260901141400, found via Kawa still reading it directly). All four merged, deployed, and verified live. Fully mirrored to KavApps kap-stable (source of truth): both migrations, plus the three readers the rename breaks there and the hand-maintained swagger.yaml that still advertised the old field name (KavApps PRs #1200, #1201 and #1206, all merged). Nothing outstanding here.

docs/issues/20260901_cad_query_material_is_a_drawing_color.md

D.39 three MCP image tools claimed a search filter covered tags; it only ever matched filenames

Severity low — no data exposure, but a described capability that silently didn’t exist, on three tools at once · Status Fixed — see PR referenced below. Not yet independently re-verified on a deployed kap-dev (see “Verification” below), so this is Fixed, not Verified.

docs/issues/20260901_image_tools_search_claimed_tags_only_did_filenames.md

D.40 certification reports an unavailable environment as a failing engine

Severity medium — no product defect, but the suite produced two false regressions in one session and the numbers were about to be reported as engine quality · Status Open

docs/issues/20260902_certification_cannot_tell_a_missing_environment_from_a_broken_engine.md

D.41 certification never writes back to the registry it is measured against

Severity medium — the registry gates which engines the product offers, and it currently asserts the opposite of the newest certification run for dataadk · Status Open

docs/issues/20260902_certification_never_writes_back_to_the_registry.md

D.42 system_type: "dataadk" resolves to a system that is not registered

Severity low today, higher if a client still sends it — no data exposure; the failure is that a retired engine id neither works nor cleanly degrades. · Status Open — found while removing the DataADK copy from KAP (PR #1142), filed rather than fixed there because the fix is a routing decision, not part of the removal.

docs/issues/20260902_dataadk_system_type_resolves_to_nothing.md

D.43 the deployed product falls back to an engine that cannot certify, and declares it verified

Severity medium — no user-visible failure is proven, but the fallback is what every user without a stored preference receives, and it is the one engine that fails three mandatory certification rows including server-side scope enforcement · Status Open — KAP side fixed (#1129); the deployed side is untouched

docs/issues/20260902_deployed_fallback_engine_is_not_certifiable.md

D.44 a tampered JWT ends the run RUN_FINISHED, not RUN_ERROR

Severity medium — a failed authentication is reported to the client as a successful run, so an expired or invalid session is indistinguishable from “you have no data”. No data is exposed; the failure is that the client cannot tell it failed. · Status Open — for the AI team. Found in KavApps main (a97ff172), running locally on :8091. Not reproduced on KAP’s engines, which pass this row.

docs/issues/20260902_tampered_jwt_finishes_the_run_instead_of_erroring.md

D.45 the in-frame mark misses the asset — the registry sits ~5 m low, and two flights’ altitudes are ~11 m high

Severity medium — the Tier 3 (framed) link and the review sheet’s crosshair are trustworthy at long range and wrong by a vessel-length up close; nothing an engineer has decided rests on them yet. · Status Calibrated 2026-09-08 (KAP #1202) — the camera is moved into the registry’s frame by data/pose-calibrations/polycarbon-unit-audit.yaml; open for the 20:3x flight (needs its own reconstruction) and for plate boxes. Two separable causes, both quantified below; neither is the photogrammetric reconstruction.

docs/issues/20260908_asset_registry_heights_and_flight_altitude_bias.md

D.46 The AI Assistant declines a gas-threshold locate question instead of answering it

Severity medium · Status Open

docs/issues/20260917_ai_assistant_declines_gas_threshold_locate_query.md

D.47 FR-APP-14’s “hard ship gate” is not enforced — no branch protection exists on KavApps main, and the workflow’s own “advisory” framing for Tier 3 doesn’t match its exit code

Severity medium · Status Open

docs/issues/20260917_tier3_eval_gate_is_not_actually_a_hard_gate.md

D.48 the assistant opens with “Yes” to a leading question, then states the correct constraint

Severity medium · Status Open — needs a product decision before it can be called a defect

docs/issues/20260921_a_conditional_yes_to_a_leading_question.md

D.49 the judge agrees with the assistant, and the verdict is thrown away for citing no evidence

Severity high · Status Open — two defects, the second needs a protocol-version decision

docs/issues/20260921_a_negative_dimension_cannot_quote_evidence.md

D.50 a fabrication pattern matches the phrase an answer quotes in order to deny it

Severity high · Status Open

docs/issues/20260921_forbidden_patterns_fire_inside_an_explicit_denial.md

D.51 material is null for all 109 assets, so six tasks can only test abstention

Severity high · Status Open — the benchmark side is repaired; the data gap is not

docs/issues/20260921_material_is_null_for_every_asset_in_the_register.md

D.52 an equipment tag’s digits plus the word “in” were read as a thickness reading

Severity high · Status Fixed — PR pending review; recorded because it is the second false positive of its class in one day

docs/issues/20260921_tag_digits_read_as_a_thickness_measurement.md

D.53 asked what it remembers on an empty thread, the assistant invents an answer

Severity high · Status Open — tracked in GitHub #411, which found it first. This record adds the 2026-09-21 reproduction and corrects a claim it made on that date.

docs/issues/20260921_the_assistant_invents_conversation_history.md

D.54 four API suites ran against a mock while believing they held a real database

Severity high · Status Fixed — all four. The gas_readings “RLS finding” was a broken fixture (2026-09-24)

docs/issues/20260923_four_api_suites_were_testing_a_mock.md

D.55 npm run test:e2e tests whatever is already on port 3000

Severity medium · Status Open

docs/issues/20260923_test_e2e_tests_whatever_is_on_port_3000.md

D.56 28 of 63 E2E tests assert against routes and labels the app no longer has

Severity medium · Status Open

docs/issues/20260923_the_e2e_suite_has_drifted_from_the_app.md

D.57 270 of flight A’s 313 asset tags name equipment the twin has no record of

Severity medium · Status Open — diagnosed, and not fixable in code

docs/issues/20260924_flight_a_photographs_equipment_the_twin_does_not_model.md

D.58 —

Severity — · Status —

docs/issues/20260924_the_asset_register_takes_40_s_to_paint.md

D.59 —

Severity — · Status —

docs/issues/20260924_the_centre_framed_rule_has_no_home.md

D.60 —

Severity — · Status —

docs/issues/20260924_the_dev_server_degrades_over_a_single_e2e_run.md

D.61 the shared migration history stopped being written on 2026-08-11, and 43 changes are live without it

Severity was high — the recorded history did not describe the database, and db push could not run · Status Resolved 2026-09-24 — the history now matches KavApps’ folder (77 recorded = 77 files) and db push works. Two related problems stay open; see Resolution

docs/issues/20260924_the_shared_migration_history_stopped_being_written.md

D.62 every image thumbnail is readable by anyone who can guess its URL

Severity high · Status Verified — bucket private since 2026-09-26 00:20 (KAP #1379 + KavApps #1290 deployed, KavApps #1289 applied)

docs/issues/20260925_the_thumbnails_bucket_is_public.md

D.63 <one line stating what is broken, not what should happen>

Severity high | medium | low · Status Open

docs/issues/TEMPLATE.md