Repository navigation
Open card tabs by key, lift the card page's rules, and fix three catalogue import issues - #1242
Merged
Merged
Conversation
Admin -> Resources links to /cards/{id}?tab=resources, but the page
read ?tab as a number, so the link opened the Card tab. Every tab was
addressed by an index worked out by hand from the tabs before it, so a
tab appearing or vanishing as its count loaded (ADRs, risks,
compliance) also moved the selection onto its neighbour.
- cardTabs.ts: the ordered tab list (cardTabKeys), resolveCardTab for a
key or an older index link, the visit rule and the extension-tab
filter. MUI Tabs now take each tab's key as its value; a tab that
vanished opens the Card tab.
- sectionConfig.ts: buildSectionOrder, now also used by the Card Layout
editor in place of its private copy, plus the hidden-field,
custom-section and calculated-field helpers.
- historyChanges.ts: one event table (label key, icon, colour), fmtVal,
resolveFieldLabel, parseChanges and eventDetailModel, a discriminated
union HistoryTab renders.
- useCardTabActivity's event map is typed with the tab keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
8 of 20 tasks
The ADR tab shows while its count loads, so the two strip assertions now wait for the strip to settle. CardDetail hands a non-numeric ?tab= to the content as a key, which resolves it against its own strip, so the deep link test asserts the key is passed on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
…card-page # Conflicts: # CHANGELOG.md # VERSION
Deploying turbo-ea-docs with
|
| Latest commit: |
d0b5046
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://38bdfefe.turbo-ea.pages.dev |
| Branch Preview URL: | https://claude-mutation-lift-card-pa.turbo-ea.pages.dev |
…alue-stream import on Business Context Sdist fallback: `wheel_url_from_pypi_payload` picked a source distribution when PyPI listed no wheel, but the extractor opens the bytes as a zip and reads the wheel's paths, so the fallback could only ever fail with a generic 502. It now raises a ValueError naming the project and version. Session across PyPI: the six update-status / update-fetch routes commit the request session before the service probes PyPI, so the pooled connection the permission check used is handed back for the round-trip; `check_remote_version_for` probes before it reads the cache and `fetch_and_cache_all` no longer commits a session it was handed (the route commits after it returns). Source-scan guards in test_db_session_holding. Write path: the three catalogue imports create their cards through `card_write_service.create_card` (`create_catalogue_card`, one savepoint per row), so an imported card gets its History entry, its hierarchy level, a reference when the type numbers cards automatically, calculations and a data-quality score. A refused row (a name another card already uses at the same level) comes back in a new `failed` list with the server's reason, and the entries beneath a refused parent are reported as "parent not imported" rather than created as roots. `ReferenceAllocator` scans each prefix once per batch, shared with `POST /cards/bulk-create`. The services no longer commit; the import routes do. The catalogue page shows the failed entries. The value-stream import was gated on a `ValueStream` type that does not exist; it is gated on Business Context, the type it creates. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
`card_write_service` reaches `extensions.bundle` through `card_approval` and `notification_service`, and `bundle` imports `version_tuple` from `catalogue_common`, so the module-level import added for `create_catalogue_card` closed a cycle: whichever side was imported first failed with a partially initialised module (eight test modules failed to collect in Backend Unit Tests). The helper now imports the write path inside the function and takes the importing user, building the actor itself, so the catalogue services and tests no longer need `WriteActor` from this module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
…ches The diff-coverage gate read 63 % on cf2f903: no API test called the three catalogues' update-status, update-fetch or import routes, so their commit-before-PyPI, commit-after-fetch and 502 paths never ran under coverage; only the capability import had a refused-row test, so the process and value-stream imports' `failed` branches were unexercised; and the plain-string refusal branch of `create_catalogue_card` had no test. Adds `tests/api/test_catalogue_update_routes.py` (parametrised over the three catalogues, services faked: admin gets the service's answer, a member gets 403, a failed download maps to 502, the import route passes the caller, the selection and the locale with the English fallback), a refused-row test for the process and the value-stream import, and a hierarchy-depth refusal case for `create_catalogue_card`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
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
Fixes the Admin → Resources link, which opened a card's Card tab instead of its Resources tab, and the tab selection jumping when a counted tab loads. To get there, the rules behind card detail move into modules with rule-by-rule tests: the tab strip, the section order and the History rows (nightly mutation backlog, #1184). It also resolves the three issues #1241 parked in
catalogue_common.py— the PyPI sdist fallback that could never work, the request's database session held across the PyPI round-trip, and the catalogue imports bypassing the card write path — plus a fourth found on the way: the value-stream import was gated on a card type that does not exist. Version 2.158.11.Type
Changes
Bugs fixed
1.
?tab=resourcesopened the Card tab.ResourcesAdmin.tsxlinks to/cards/{id}?tab=resources, butCardDetail.tsxread the parameter withparseInt(…) || 0. The parameter is now passed through as given. A key is resolved against the strip; an index (?tab=1for a process's flow, the BPM and navigator links) still works.2. The open tab jumped when a tab before it appeared or vanished. Tabs were addressed by index, each position worked out by hand from the tabs before it. ADRs, Risks and Compliance show while their count loads and go once it settles at 0, so the selection moved onto the neighbouring tab. MUI
Tabsnow take each tab's key as its value, and a tab that has vanished opens the Card tab.Lifted into tested modules
features/cards/cardTabs.ts(new):cardTabKeys: the ordered list from card type, module flags, permissions and counts;hasItemsOrLoading;resolveCardTab: a key, a numeric index or digits, anything else → Card;notesVisit: no PPM, no extension tabs;visibleExtensionTabs/extensionTabValue;CARD_TAB_LABEL_KEYS.useCardTabActivity's event map is now typed with these keys, so a renamed tab fails to compile there.features/cards/sectionConfig.ts(extended):buildSectionOrder;customSectionsOf,hiddenFieldKeys,allFieldsHidden,calculatedFieldKeys.The Card Layout editor now calls
buildSectionOrderinstead of its private, untested copy (getSectionOrder), so the editor and the card cannot disagree on order.features/cards/sections/historyChanges.ts(new):EVENT_METAtable (label key, icon, colour), replacing two parallel tables;eventMeta,fmtVal,resolveFieldLabel,parseChanges,fieldLabelsFor;eventDetailModel, a discriminated union (relation / risk / link / processFlow / plain) thatHistoryTabrenders.CardDetailContentandHistoryTabkeep the fetch effects, the dirty tracking and all rendering. Markup and i18n keys are unchanged.Catalogue fixes (from #1241's parked list)
3. The PyPI sdist fallback could never work.
wheel_url_from_pypi_payloadpicked a source distribution when a release listed no wheel, butextract_all_catalogues_from_wheelopens the bytes withzipfileand reads the wheel'sturbo_ea_capabilities/data/*.jsonpaths — a.tar.gzhas neither the container nor the layout, so the branch only ever produced a generic 502 "Catalogue fetch failed" (and the one test of it served zip bytes under a.tar.gzURL). The helper now takes a wheel only and raisesValueError("PyPI lists no wheel for turbo-ea-capabilities <version>"); nothing is requested or cached for such a release.4. The request's session was held across the PyPI round-trip.
require_permissionhad already queried on the request session, so a pooled connection was checked out whilecheck_remote_version_forprobed PyPI (30 s timeout) andfetch_and_cache_alldownloaded and parsed a wheel — and the latter committed a session it was handed. Followingrun_extension_store_check_now: the sixGET …/update-status/POST …/update-fetchroutesawait db.commit()before calling the service,update_fetchcommits again after it returns (inside itstry, so a failed commit still maps to the 502),check_remote_version_forprobes before it reads the cache, andfetch_and_cache_allno longer commits. Response JSON is unchanged.5. The three imports bypassed the card write path.
import_capabilities,import_processesandimport_value_streamsbuiltCardrows directly and committed inside the service, so an imported card had no History entry (the CLAUDE.mdupdated_atrule), nohierarchyLevel, no reference inautomode, a data-quality score of 0 until next touched, acapabilityLevelcopied from the catalogue rather than its real depth, and could land as a same-named sibling the UI refuses. They now go throughcard_write_service.create_cardviacatalogue_common.create_catalogue_card— one savepoint per row, thePOST /cards/bulk-createshape — with aReferenceAllocator(card_reference.py) that scans each prefix once per batch and is shared with bulk create (_bulk_assign_referenceis now a call to it;create_cardtakes it as an optional argument, so every other caller is byte-identical). A refused row comes back in a newfailed: [{catalogue_id, reason}]list with the server's message, and the entries beneath a refused parent are reported asparent not importedinstead of being created as roots. The services no longer commit; the import routes do. Two visible behaviour changes: a capability imported without its catalogue parent is an L1 root card (as every other write path labels it; the catalogue's level stays readable fromcatalogueId), and an entry whose name another card of the type already uses at the same level is reported and not imported — macros included, since they never match by name (#1241). The catalogue page shows the failed entries with their reasons (newimportFailedBodykey, 10 locales); the three guides document the behaviour (10 locales each).create_catalogue_cardimports the write path inside the function:card_write_servicereaches the extension bundle module throughcard_approval→notification_service, and that module importsversion_tuplefromcatalogue_common, so a module-level import closed a cycle (the first CI run failed to collect eight test modules on it).6. The value-stream import was gated on
ValueStream.POST /value-stream-catalogue/importrequiredinventory.createon aValueStreamtype that does not exist, so a per-type Create deny on Business Context — the type it creates — did not apply. It is gated onBusinessContext.Test Plan
cardTabs.test.ts:resolveCardTabfor keys, numbers, digits and garbage;notesVisit; the extension filter by type and permission.sectionConfig.test.ts:buildSectionOrderwith no stored order or an empty one; stored order plus custom sections it does not name; successors and tags spliced before relations or appended; a stored hierarchy or successors the type no longer has;historyChanges.test.ts:fmtValfor every value shape;resolveFieldLabelprecedence and theattr_prefix;parseChanges: scalars, skipped malformed entries, attribute and lifecycle whole-dict diffs, anullside, approval labels with the raw fallback;fieldLabelsFor([bug] Failure to link a document in resources after deleting one #1166);eventDetailModelfor every event family and its fallbacks.CardDetailContent.tabs.test.tsx(new, real component with marker stubs):"resources"key link;CardDetail.test.tsx: regressionopens the tab a link names by key (Admin → Resources links ?tab=resources).HistoryTab.test.tsx: one test per event family: relation peer link, risk reference link and level, document link in a new tab, withdrawn flow's revision and reason, unknown event, per-attribute rows, empty history.Catalogue, backend:
tests/api/test_catalogue_update_routes.py(new, parametrised over the three catalogues with the services faked): an admin gets the service's answer fromupdate-statusandupdate-fetch; a member gets 403 on both without the service running; a failed download maps to 502Catalogue fetch failed; the import route passes the caller, the selection and the body's locale to the service, and falls back to English without one. These are what lifted the diff-coverage gate: its first run read 63 % because no API test reached any of the nine route bodies.test_catalogue_common.py:TestWheelUrlFromPypiPayload(the wheel is chosen whatever its position; sdist-only, a wheel entry with no URL, nourlsand a missinginfo.versioneach raise with the literal message);TestCreateCatalogueCard(a created card carries the parent as a UUID,capabilityLevelrecomputed from depth,created_by, DRAFT and onecard.createdevent; a refused row rolls back alone, returns the 409 message rather than the detail dict, and the next row still lands; a plain-string refusal — the hierarchy depth guard — comes back untouched with its row gone).test_capability_catalogue_service.py:test_fetch_remote_catalogue_refuses_a_release_without_a_wheelreplaces the sdist test (raises, nothing cached, only the index requested);test_import_writes_cards_through_the_shared_write_path(ahas_hierarchytype withreference_configautoBC-: referencesBC-0001…0003,hierarchyLevel1/2/3,capabilityLevelL1/L2/L3, a weighted field raisingdata_quality, onecard.createdeach);test_a_refused_entry_and_the_entries_below_it_are_reported(failed= the taken name and its child withparent not imported, neither created);test_macro_matched_by_catalogue_id_only_not_by_namenow asserts the macro is reported with the 409 reason, nothing is created and the customer's card is untouched;capabilityLevelexpectations follow the card's real depth.test_process_catalogue_service.py/test_value_stream_catalogue_service.py: an imported branch carrieshierarchyLevel1/2 and onecard.createdper card; a stage pulls in its stream; and, in each, a refused entry (a shared name a root card already uses) and the entries beneath it come back infailed, none created.test_card_reference.py:ReferenceAllocatorcontinues after the highest reference in use, scans once per prefix (monkeypatchedscan_highest_for_prefixrecords["APP-", "PRC-"]), and leaves anoff-mode or missing type without a reference.test_updated_at_invariant.py::TestImportPathsRecordThemselves: a catalogue import records exactly onecard.createdwith the importer'suser_id.test_db_session_holding.py(source scans): each of the six routes commits before its service call,update_fetchcommits again after it,check_remote_version_forasks PyPI before it reads the cache,fetch_and_cache_alland the three imports contain nodb.commit(), and each import route commits afterawait svc.import_.test_cards_type_permissions.py::TestCatalogueImporterGate: the value-stream import returns 403 for a role with a Business Context Create deny and not for one that allows it.Catalogue, frontend:
CataloguePage.test.tsx— a response withfailedshows the count andBC-2 — <reason>; a response from a backend older than 2.158.11 (nofailedkey) renders the summary alone.Local checks:
ruff format/ruff checkclean;tsc -band eslint on the touched files are clean (the four pre-existingno-explicit-anywarnings inCataloguePage.tsxand the two Card Layout editor warnings predate this change);python scripts/dump_openapi.pyleavesdocs/api/openapi.jsonunchanged (the import routes return plain dicts); every module in the former import cycle imports cleanly when imported first in a fresh interpreter. As agreed, the test suites run in CI (Backend Integration, Frontend Tests, E2E and the mutation jobs).All CI checks pass (backend lint, backend tests, frontend lint, frontend build, frontend tests, mutation tests)
Manually tested the affected feature
Added/updated tests for new or changed behavior
Mutation gate: my tests kill the mutants on the lines I changed (
make mutation-diff). Score on the merged head5b1d1c597: backend 92.1 % of 229 changed-line mutants (card_reference.py100 %,card_write_service.py100 %,catalogue_common.py96.4 %,value_stream_catalogue_service.py90.0 %,capability_catalogue_service.py89.7 %,process_catalogue_service.py83.7 %); frontend 87.9 % of 672 (sectionConfig.ts96.7 %,historyChanges.ts92.3 %,cardTabs.ts91.2 %,CataloguePage.tsx82.4 %,HistoryTab.tsx71.7 %,CardDetailContent.tsx59.4 %). Survivors kept, none pragma'd: inCardDetailContent.tsxthe per-tabtab === "x"render guards (each panel's mount condition — swapping one renders the same panel twice, which the marker-stub tests cannot tell apart) and the?.optional chains ontypeConfig(equivalent: the type is loaded before the component renders); inCataloguePage.tsxtwosxobjects and the?? []fallback array; in the backend theLEVEL_TO_SUBTYPE.get(…, "process")default literal,allocator=None(the per-card reference path gives the same numbers with one scan per card instead of one per prefix),failed_ids.add(None)andcontinue → breakon the last node of a refused chain, and twocatalogue_commonliterals. Floors added infloors.toml[modules]:cardTabs.ts89,historyChanges.ts90,sectionConfig.ts94.Checklist
CLAUDE.mdasync deffor all new route handlers and DB operations/VERSIONand added aCHANGELOG.mdentry (if user-facing change)docs/(if UI or feature change)🤖 Generated with Claude Code
https://claude.ai/code/session_014eZ98kaESE7YBa84fT1msc
Generated by Claude Code