Skip to content

fix(portability): code every export/import refusal, and stop rendering 5xx boilerplate - #6066

Open
leonlaiyc wants to merge 2 commits into
kirodotdev:mainfrom
leonlaiyc:fix/portability-error-codes
Open

fix(portability): code every export/import refusal, and stop rendering 5xx boilerplate#6066
leonlaiyc wants to merge 2 commits into
kirodotdev:mainfrom
leonlaiyc:fix/portability-error-codes

Conversation

@leonlaiyc

@leonlaiyc leonlaiyc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Problem / Motivation

AGENTS.md states the rule directly: "Backend-owned strings have no catalog
path yet, so a new non-2xx JSON body MUST carry a machine-readable code
field."
error-code-baseline.json — the worklist that ratchets the existing
debt down — listed dashboard/handlers/portability.py with missing_code: 9.

The file already had the answer in it. Its 413 answers:

{"error": "archive too large (max 2048 MB)", "code": "import_archive_too_large"}

The other nine refusals across export, import and preview carried prose only.

Why it matters

Two different failures are indistinguishable to a client today. A 400 from
/api/portability/import is either a rejected mode or a rejected archive; a 500
from any of the three endpoints is opaque by design. And PortabilityTab
renders data.error || <localized fallback>, so the English produced in Python
wins over a catalog string that says the same thing in the reader's language —
in a dashboard translated into 12.

What changed (motivation → approach → change)

Backend. The nine refusals gain codes in the vocabulary already used here
and elsewhere in the tree: auth_required (the wire name from
handlers_cloud.py, on all three endpoints), file_field_required,
invalid_import_mode, import_archive_invalid, and one <operation>_failed
per handler — export_failed, import_failed, preview_failed. Every prose
string is unchanged.

The two kinds of 4xx are deliberately kept apart, and this is the decision
the rest of the PR follows from:

  • import_archive_invalid carries the archive validator's own finding —
    "manifest.json is missing". That prose is the value of the message.
  • The 5xx bodies carry boilerplate — "Export failed" — which is opaque on
    purpose, so the cause stays private.

Frontend. refusalText() inverts the preference for exactly one case: a
refusal that is both coded and 5xx. There the localized fallback wins,
because the server's string says strictly less than the fallback already says.
A coded 4xx keeps its detail. An uncoded refusal at any status keeps its
message — it may be from a proxy or an edge rather than these handlers, and
there the prose can be all there is.

That is one predicate on (code, status), not a hand-maintained list of codes
the frontend has to keep in sync with the backend.

The export path is deliberately not moved. Its fallback is
resp.statusText, not a catalog string, so there is nothing localized to
prefer. Giving it one means authoring new user-facing copy in 12 languages,
which is a different change from this contract.

The user-visible effect is confined to two error messages in the Portability
tab: where a failed import or preview read Import failed / Preview failed
in English, it now reads the tab's own localized string. No panel, component,
layout or theme changed.

Baseline

missing_code 1262 -> 1253 -- exactly the nine -- and the
dashboard/handlers/portability.py entry is gone. _compliant moves
1410 -> 1419, exactly +9.

(Numbers above reflect the 2026-08-30 rebase onto main 750e4b15c; the
committed baseline there was already fresh, so both deltas are exactly this
PR's ±9 with no inherited refresh. An earlier revision of this section
described the older 420dbebcf rebase state.)

Rebased onto current main 750e4b15c. The
baseline was regenerated from the live tree with the repo's own
python test/test_error_code_contract.py --update, never hand-merged: the
totals in this file are relative to this PR's base, so adding or reconciling
numbers across branches would be meaningless. Diffing the regenerated file
against main's shows exactly one changed entry --
dashboard/handlers/portability.py, removed -- and no other file added,
removed, or changed. missing_code, the number the gate enforces, moves by
exactly nine.

Tests

test/test_api_portability.py is new — the existing test_portability.py
covers the zip logic, and nothing exercised these handlers over HTTP. It drives
them through an aiohttp TestServer with real multipart uploads, so the auth
and file-part paths are the ones a client actually hits.

Test Locks in
test_every_endpoint_denies_anonymously_with_auth_required 401 → auth_required on all three endpoints
test_export_failure_is_coded_and_stays_opaque export_failed, prose still generic, private path still absent
test_an_unrecognized_import_mode_is_coded invalid_import_mode, four rejected modes
test_an_upload_without_the_file_part_is_coded file_field_required on both upload endpoints
test_a_rejected_archive_keeps_the_validator_detail import_archive_invalid alongside the validator's message — the case the frontend rule turns on
test_import_failure_is_coded / test_preview_failure_is_coded import_failed / preview_failed, cause still not leaked
test_every_refusal_carries_a_code per-file ratchet: no path may regress to prose-only

Red before: with only the handler change stashed, all 14 cases fail.
Green after: 14 pass.

website/src/pages/overview/PortabilityTab.test.tsx covers refusalText
directly — 4 cases, including the two negative controls that define the rule's
edges: a coded 4xx keeps its detail, and an uncoded 5xx keeps its message.

Other gates: test_portability.py and test_error_code_contract.py pass
alongside the new module (68 passed / 3 skipped). tsc -b and eslint clean;
flake8, isort, mypy clean; scripts/check_black_formatting.py passes on
the 5 changed files.

Coverage

The rebased head also tripped the Coverage Gate, in the good direction: this PR's new test/test_api_portability.py takes dashboard/handlers/portability.py from 17.0% to 82.4%, above the 80% per-file floor, so the gate asked for its .github/coverage-baselines/backend.txt entry to be removed or the baseline stops shrinking. Pruned with the repo's own updater against that run's coverage artifact rather than a local run, whose rates differ and would prune the wrong entries. The refresh only deletes, so the diff is exactly one line.

Manual verification

N/A — unit coverage sufficient: the backend tests exercise the real handlers
over HTTP including multipart, and the one user-visible surface is a string
chosen by refusalText, which is asserted directly at every branch.

Screenshots / video

The frontend half changes what the error banner renders for a 5xx that carries a code: the server's English boilerplate is replaced by the localized catalog string. The Japanese pair is the one that shows why it matters — before, a fully translated panel with a bare English sentence in it.

Japanese (ja-JP)
Before (main) — server prose: Preview failed before, ja
After (this PR) — localized: 無効なアーカイブ after, ja
Same pair in English, where the delta is smaller but present (Preview failedInvalid archive)
English (en-US)
Before (main) before, en
After (this PR) after, en

How these were captured, stated plainly: the real PortabilityTab component, the real src/index.css, and the real i18n catalog, with the real file-input onChange handler driven to run the real preview path. Only the network response is stubbed — to {"ok": false, "error": "Preview failed", "code": "preview_failed"} with status 500 — because a specific server response is precisely what this change is about. The capture harness was temporary and is not in the diff; only the four PNGs are, under the ephemeral top-level temp-screenshots/ the description contract specifies.

Related Issues

No issue — this is the dashboard/handlers/portability.py line item from
error-code-baseline.json's own worklist ("drive missing_code to zero, one PR
per file or per directory, moving each frontend consumer in the same PR").

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable) — N/A, no doc surface changes
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

@leonlaiyc
leonlaiyc requested a review from a team August 26, 2026 14:13
@leonlaiyc
leonlaiyc requested a review from a team as a code owner August 26, 2026 14:13
@leonlaiyc
leonlaiyc requested a review from patrigao August 26, 2026 14:13
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 26, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/portability-error-codes branch from 6d70bef to 74f17c4 Compare August 27, 2026 01:33
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Aug 27, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/portability-error-codes branch from 9475834 to 639768b Compare August 27, 2026 04:19
@github-actions github-actions Bot added merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Aug 27, 2026
@leonlaiyc
leonlaiyc force-pushed the fix/portability-error-codes branch from 639768b to ac67704 Compare August 27, 2026 07:27
@github-actions github-actions Bot added merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Aug 27, 2026
@bolichen97
bolichen97 enabled auto-merge August 30, 2026 00:00
@bolichen97 bolichen97 added needs-pr-triage PR scanner: awaiting automated triage and removed needs-pr-triage PR scanner: awaiting automated triage labels Aug 30, 2026
@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention labels Aug 30, 2026
@bolichen97
bolichen97 force-pushed the fix/portability-error-codes branch from 5e0b85d to c07de0b Compare August 30, 2026 21:32
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed allow-fork-workflow-change readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Aug 30, 2026
bolichen97
bolichen97 previously approved these changes Aug 30, 2026

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto current main and resolved the conflict. The only conflict was the generated error-code-baseline.json; I took main's copy and then regenerated it with python test/test_error_code_contract.py --update so the counts describe the actual post-rebase tree rather than either side's stale snapshot. The ratchet still moves in the only permitted direction. Verified locally: the error-code contract suite and this PR's own API tests pass. Approving.

@github-actions github-actions Bot added merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 31, 2026
@bolichen97
bolichen97 disabled auto-merge September 3, 2026 21:31
@bolichen97
bolichen97 enabled auto-merge (squash) September 3, 2026 21:31
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • This PR is OVERLAPPING with PR #7273. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6066: REBASE. Different layers of the same refusal; 6066's contract is prose-agnostic, so 7273's new messages need no follow-up in 6066. Files: src/kiro_crew/dashboard/handlers/portability.py. The two independent directions used different labels; the matrix conservatively retains OVERLAPPING for coordination.
  • This PR is OVERLAPPING with PR #7308. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6066: REBASE. Purely mechanical collision on a generated file; whichever lands second regenerates the baseline with python test/test_error_code_contract.py --update. Files: error-code-baseline.json.
  • This PR is OVERLAPPING with PR #8315. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6066: REBASE. Complementary changes to one handler with disjoint hunks; whichever lands second needs no rework beyond ordinary context adjacency. Files: src/kiro_crew/dashboard/handlers/portability.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

leonlaiyc and others added 2 commits September 8, 2026 21:21
…g 5xx boilerplate

`error-code-baseline.json` listed `dashboard/handlers/portability.py` with
`missing_code: 9` — nine refusals whose only identity was an English sentence.
The file already had the pattern: its 413 answers
`{"error": "archive too large (max 2048 MB)", "code": "import_archive_too_large"}`.
The other nine now follow it — `auth_required` (the wire name from
`handlers_cloud.py`), `file_field_required`, `invalid_import_mode`,
`import_archive_invalid`, and one `<operation>_failed` per handler.

The two kinds of 4xx here are deliberately kept apart. `import_archive_invalid`
carries the archive validator's own finding ("manifest.json is missing"), which
is the whole value of that message; the 5xx bodies carry opaque boilerplate
("Export failed") that exists precisely so the cause stays private.

`PortabilityTab` renders `data.error || <localized fallback>`, so it preferred
that English boilerplate over a catalog string that says the same thing in the
reader's language. `refusalText()` inverts it for exactly one case — a refusal
that is BOTH coded and 5xx — leaving 4xx detail and uncoded refusals of any
status untouched. The export path is not moved: its fallback is
`resp.statusText`, so there is no localized string to prefer, and writing one
is new user-facing copy rather than this contract.

Drive-to-green (rebase onto current main): original work by Leon (leonlaiyc);
resolved the error-code-baseline.json conflict by taking main's counters and
regenerating with `python test/test_error_code_contract.py --update`
(missing_code 1262 -> 1253, the portability entry deleted). All source changes
kept verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
…idence

Two CI gates on the rebased head, both of them fair.

Coverage Gate: this PR's new test/test_api_portability.py takes
dashboard/handlers/portability.py from 17.0% to 82.4%, above the 80% per-file
floor, so its baseline entry has to go or the baseline stops shrinking. Pruned
with the repo's own updater against THIS run's coverage artifact (a local
coverage run has different rates and would prune the wrong entries); the
refresh only deletes, so exactly one line is removed.

Screenshot Evidence: the PR changes PortabilityTab.tsx, a user-visible surface,
and the change IS visible -- on a 5xx carrying a code the banner now renders the
localized catalog string instead of the server's English boilerplate. Captured
from the real component with real styles and the real i18n catalog, driving the
real file-input handler; only the network response is stubbed, since a specific
server response is what the change is about. The Japanese pair is the telling
one: before, a fully translated panel with a bare English "Preview failed".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bolichen97

Copy link
Copy Markdown
Collaborator

Rebased onto main 6fa5519c by a maintainer as part of the 2026-09-08 open-PR audit. No behaviour change; the nine coded refusals and refusalText() are exactly as you wrote them.

Conflicts resolved (both generated baselines, took main's version and re-applied only this PR's own edit):

  • error-code-baseline.json: regenerated with test/test_error_code_contract.py --update; net effect is missing_code 1137 to 1128 and the dashboard/handlers/portability.py entry dropped. _compliant kept at main's number plus 9 rather than the full fresh count, to avoid sweeping in unrelated drift.
  • .github/coverage-baselines/backend.txt: main had already removed the agents/cron/prompts lines; only the portability.py 17.0 exemption line is removed here.

Gates run locally on changed files: black, isort, flake8, pytest test/test_api_portability.py test/test_error_code_contract.py (20 passed), tsc --noEmit, and PortabilityTab.test.tsx (4 passed).

Please review the resolution. A maintainer push makes the maintainer the last pusher, so a second approver is needed under the repo's last-push rule. Reply if anything looks wrong.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge labels Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drive-to-green PR claimed by drive-to-green pipeline fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants