From 6a556b0df2786219e44ffbeeb9d92379ed68502f Mon Sep 17 00:00:00 2001 From: Matthew Peterkort <33436238+matthewpeterkort@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:00:18 +0000 Subject: [PATCH 1/2] refactor(loom-ui): move coded Group picker out of the reshape editor ConstructionReshapeEditor coordinated both the selected-operation form and a distinct asynchronous picker workflow (direct-source coded Group selection) that owns inventory readiness polling, source pagination, saved-value reauthorization, search and paging. The picker had one callsite and no dependency on the parent's mutable request refs; its reasoning burden was paid by every reader of the 5,115-line multi-operation module. Move GroupCodedValuePicker, its private readiness/paging state types, the readiness poll waiter, the initial-choices constant, and the availability classifier into GroupCodedValuePicker.tsx with an explicit exported props type. Shared Group form construction and candidate-evaluation helpers (codedFamilyKey, codedCategoryKey, createOpaqueId, uniqueName, getUsedGroupNames, ensureGroupCodedPassthroughs) and the form/context types stay in the parent module and are now imported by the picker; no logic was rewritten. The parent drops its now-unowned useQuery, semanticConceptLabel, FrameSourceOption, SemanticInventoryBrowseResponse and SemanticInventoryItem imports and shrinks to 4,601 lines. Behavior is byte-exact: the moved bodies were checked-extracted verbatim (rerunnable tool asserts before/after slice equality), no expectations were touched, and no comments were added. Verification: - ConstructionReshapeEditor.unit.test.tsx: 103/103 passed, 0 skipped - constructionOperations directory: 18 files, 314/314 passed - ExplorerBuilder feature suite: 79 files, 1,076/1,076 passed - BuilderWorkspace operation-history/reconciliation/suggestions: 98/98 - operation-history formal journey replay (UI + Go phases): passed - typecheck (tsc -p tsconfig.test.json), check-boundaries, check_no_code_comments: clean; git diff --check clean - Builder browser acceptance: not run (no Chromium in this sandbox); recorded as incomplete evidence Closes #199 (source-map re-pin for reviewed-sources.json and the UI contract inventory citations is handed to the verifier/tooling owner with the verified line mapping). --- .../ConstructionReshapeEditor.tsx | 536 +----------------- .../GroupCodedValuePicker.tsx | 534 +++++++++++++++++ 2 files changed, 545 insertions(+), 525 deletions(-) create mode 100644 ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx index 02ddf60d0..fdadd9669 100644 --- a/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'; -import { useQuery } from '../../../react'; import type { LoomClient } from '../../../api'; import type { Construction, @@ -9,14 +8,11 @@ import type { ConstructionTableScalar, ConstructionStep, ExplorerBuilderCatalog, - FrameSourceOption, - SemanticInventoryBrowseResponse, - SemanticInventoryItem, } from '../../../types'; import { constructionSchema } from '../../../types'; import { RelatedExpandEditor, type RelatedExpandQueryOwner } from './RelatedExpandEditor'; import { CodedPivotEditor } from './CodedPivotEditor'; -import { semanticConceptLabel } from '../catalogItems'; +import { GroupCodedValuePicker } from './GroupCodedValuePicker'; import { classifySourcePickerEmptiness, sourcePickerBoundSet } from './sourcePickerExplanations'; import { existingColumnForFieldPath } from './existingFieldReuse'; import { SourcePickerExplanation } from '../components/SourcePickerExplanation'; @@ -67,7 +63,7 @@ type PivotOperation = Extract; type ReshapeColumn = ConstructionStageDescriptor['columns'][number]; -type ReshapeStage = ConstructionStageDescriptor; +export type ReshapeStage = ConstructionStageDescriptor; type ReshapeCapabilities = ConstructionCapabilitiesResponse; type SourceInputChoice = NonNullable['choices'][number]; type AvailableSourceField = { @@ -76,7 +72,7 @@ type AvailableSourceField = { }; type StageCapability = { readonly supported: boolean; readonly reason: string; readonly reasonCode?: string }; -type GroupKey = { +export type GroupKey = { readonly inputColumnId: string; readonly outputColumnId: string; readonly name: string; @@ -128,7 +124,7 @@ type GroupSemanticKey = { readonly scopeLabel: string; }; -type GroupCodedCategory = { +export type GroupCodedCategory = { readonly key: string; readonly familyKey: string; readonly system: string; @@ -230,7 +226,7 @@ type UnpivotForm = { export type ConstructionReshapeStep = ConstructionStep; -type GroupForm = { +export type GroupForm = { readonly kind: 'group'; readonly stepId: string; readonly missingKeyPolicy: 'GROUP' | 'EXCLUDE' | 'ERROR'; @@ -387,7 +383,7 @@ type AwaitingPivotDiscovery = PendingPivotDiscovery & { export const CONSTRUCTION_RESHAPE_EDITABLE_KINDS = ['PIVOT', 'CODED_PIVOT', 'UNPIVOT', 'GROUP', 'CODED_GROUP', 'EXPAND', 'RELATED_EXPAND'] as const; -const createOpaqueId = (prefix: string): string => { +export const createOpaqueId = (prefix: string): string => { const randomPart = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2); return `${prefix}_${randomPart}`; }; @@ -494,83 +490,10 @@ const normalizedName = (value: string): string => { return /^[0-9]/.test(normalized) ? `column_${normalized}` : normalized; }; -const codedFamilyKey = (bindingId: string, sourcePath: string): string => `${bindingId}\u0000${sourcePath}`; -const codedCategoryKey = (familyKey: string, system: string, code: string): string => `${familyKey}\u0000${system}\u0000${code}`; -const INITIAL_VISIBLE_CODED_CHOICES = 10; -const CODED_INVENTORY_READINESS_POLL_INTERVAL_MS = 1_000; +export const codedFamilyKey = (bindingId: string, sourcePath: string): string => `${bindingId}\u0000${sourcePath}`; +export const codedCategoryKey = (familyKey: string, system: string, code: string): string => `${familyKey}\u0000${system}\u0000${code}`; -const waitForCodedInventoryReadinessPoll = (signal: AbortSignal): Promise => new Promise((resolve) => { - if (signal.aborted) { - resolve(); - return; - } - let timer: number | undefined; - const finish = () => { - if (timer !== undefined) window.clearTimeout(timer); - signal.removeEventListener('abort', finish); - resolve(); - }; - timer = window.setTimeout(finish, CODED_INVENTORY_READINESS_POLL_INTERVAL_MS); - signal.addEventListener('abort', finish, { once: true }); -}); - -const availableGroupCodedEntry = ( - item: SemanticInventoryItem, - sources: ReadonlyArray, -) => { - const source = sources.find((candidate) => candidate.resourceType === item.resourceType - && candidate.bindingId === item.bindingId && candidate.sourcePath === item.sourcePath); - const semanticSource = item.constructionChoice?.source; - if (!source || !item.code || item.codingVersion || !item.constructionChoice || item.constructionChoice.route.length !== 0 - || semanticSource?.kind !== 'SEMANTIC' || semanticSource.resourceType !== source.resourceType - || semanticSource.bindingId !== source.bindingId || semanticSource.sourcePath !== source.sourcePath - || semanticSource.code !== item.code || (semanticSource.system ?? '') !== item.system - || item.readiness.status !== 'READY' && item.readiness.status !== 'READY_WITH_WARNING') return undefined; - return { item, source, familyKey: codedFamilyKey(source.bindingId, source.sourcePath) }; -}; - -type GroupCodedSourceDiscovery = { - readonly scopeKey: string; - readonly sources: ReadonlyArray; - readonly complete: boolean; - readonly truncated: boolean; - readonly loading: boolean; -}; - -type GroupCodedInventoryReadiness = { - readonly scopeKey: string; - readonly contextToken: string; - readonly buildId: string; - readonly state: SemanticInventoryBrowseResponse['state']; -}; - -type GroupCodedInventoryFamily = { - readonly source: FrameSourceOption; - readonly entries: SemanticInventoryItem[]; - readonly seenCursors: Set; - started: boolean; - cursor?: string; - exhausted: boolean; -}; - -type GroupCodedInventoryCheckpoint = { - readonly scopeKey: string; - readonly families: Map; - contextToken?: string; - buildId?: string; - initialized: boolean; - loadedRound: number; -}; - -type GroupCodedInventoryView = { - readonly scopeKey: string; - readonly sourceKey: string; - readonly entries: ReadonlyArray; - readonly hasMore: boolean; - readonly complete: boolean; -}; - -const uniqueName = (base: string, used: ReadonlySet): string => { +export const uniqueName = (base: string, used: ReadonlySet): string => { const normalized = normalizedName(base); if (!used.has(normalized)) return normalized; let suffix = 2; @@ -1778,7 +1701,7 @@ const allDiscoveredPivotCategoriesSelected = ( return discovery.categories.every((category) => selectedIdentities.has(scalarIdentity(category.key))); }; -const getUsedGroupNames = (form: GroupForm, excludingColumnId?: string): ReadonlySet => +export const getUsedGroupNames = (form: GroupForm, excludingColumnId?: string): ReadonlySet => new Set([ ...form.keys.filter((key) => key.outputColumnId !== excludingColumnId).map((key) => key.name.trim().toLowerCase()), ...form.fieldKeys.filter((key) => key.outputColumnId !== excludingColumnId).map((key) => key.name.trim().toLowerCase()), @@ -1787,7 +1710,7 @@ const getUsedGroupNames = (form: GroupForm, excludingColumnId?: string): Readonl ...form.rowValues.filter((value) => value.outputColumnId !== excludingColumnId).map((value) => value.name.trim().toLowerCase()), ]); -const ensureGroupCodedPassthroughs = ( +export const ensureGroupCodedPassthroughs = ( form: GroupForm, stage: ReshapeStage, inputColumnIDs: ReadonlyArray, @@ -3725,443 +3648,6 @@ const GroupEditor = (props: { ); }; -const GroupCodedValuePicker = (props: { - readonly context: NonNullable; - readonly form: GroupForm; - readonly stage: ReshapeStage; - readonly disabled: boolean; - readonly onChange: (form: GroupForm) => void; -}) => { - const { client, project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot } = props.context; - const [pageRound, setPageRound] = useState(0); - const [sourceProgress, setSourceProgress] = useState(); - const [inventoryProgress, setInventoryProgress] = useState(); - const [readinessProgress, setReadinessProgress] = useState(); - const checkpointRef = useRef(undefined); - const requestedRoundRef = useRef(0); - const currentForm = useRef(props.form); - currentForm.current = props.form; - const changeForm = useRef(props.onChange); - changeForm.current = props.onChange; - - const sourceScopeKey = JSON.stringify([project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot]); - const readinessQuery = useQuery(async (signal) => { - const browseReadiness = () => client.browseSemanticInventory({ - project, explorerId, authResourcePath, snapshotToken, rowRoot, limit: 1, - }, signal); - let response = await browseReadiness(); - while (response.state === 'running' && !signal.aborted) { - setReadinessProgress({ - scopeKey: sourceScopeKey, - contextToken: response.contextToken, - buildId: response.buildId, - state: response.state, - }); - await waitForCodedInventoryReadinessPoll(signal); - if (!signal.aborted) response = await browseReadiness(); - } - return { - scopeKey: sourceScopeKey, - contextToken: response.contextToken, - buildId: response.buildId, - state: response.state, - } satisfies GroupCodedInventoryReadiness; - }, [client, sourceScopeKey]); - const readiness = readinessQuery.data?.scopeKey === sourceScopeKey - ? readinessQuery.data - : readinessProgress?.scopeKey === sourceScopeKey ? readinessProgress : undefined; - const inventoryReady = Boolean(readiness?.buildId && readiness.state === 'complete'); - const inventoryScopeKey = JSON.stringify([sourceScopeKey, readiness?.contextToken, readiness?.buildId]); - const sourcesQuery = useQuery(async (signal) => { - const sources = new Map(); - const seenCursors = new Set(); - let cursor: string | undefined; - let complete = false; - let truncated = false; - do { - const response = await client.browseFrameSourceOptions({ - project, explorerId, authResourcePath, snapshotToken, outputId, resourceType: rowRoot, limit: 50, - ...(cursor ? { cursor } : {}), - }, signal); - if (response.snapshotToken !== snapshotToken || response.outputId !== outputId) { - throw new Error('Coded source fields were returned for a different table snapshot. Reload the table and retry.'); - } - for (const source of response.sources) { - if (source.route.length === 0 && source.resourceType === rowRoot) sources.set(source.choiceId, source); - } - complete = response.complete; - truncated = truncated || response.truncated; - cursor = response.nextCursor; - const progress = { - scopeKey: inventoryScopeKey, - sources: [...sources.values()], - complete: complete && !cursor, - truncated, - loading: Boolean(cursor), - } satisfies GroupCodedSourceDiscovery; - if (!signal.aborted) setSourceProgress(progress); - if (cursor && seenCursors.has(cursor)) throw new Error('Coded source fields could not be completely listed. Reopen the editor to retry.'); - if (cursor) seenCursors.add(cursor); - } while (cursor && !signal.aborted); - return { - scopeKey: inventoryScopeKey, - sources: [...sources.values()], - complete: complete && !cursor, - truncated, - loading: false, - } satisfies GroupCodedSourceDiscovery; - }, [client, inventoryScopeKey], inventoryReady); - - const sourceDiscovery = sourcesQuery.data?.scopeKey === inventoryScopeKey - ? sourcesQuery.data - : sourceProgress?.scopeKey === inventoryScopeKey ? sourceProgress : undefined; - const sources = sourceDiscovery?.sources ?? []; - const sourceKey = JSON.stringify(sources.map((source) => [source.choiceId, source.bindingId, source.sourcePath])); - const currentInventoryScopeKey = useRef(inventoryScopeKey); - currentInventoryScopeKey.current = inventoryScopeKey; - const inventoryQuery = useQuery(async (signal) => { - let checkpoint = checkpointRef.current; - if (!checkpoint || checkpoint.scopeKey !== inventoryScopeKey - || checkpoint.contextToken !== readiness?.contextToken || checkpoint.buildId !== readiness?.buildId) { - checkpoint = { - scopeKey: inventoryScopeKey, - families: new Map(), - contextToken: readiness?.contextToken, - buildId: readiness?.buildId, - initialized: false, - loadedRound: 0, - }; - checkpointRef.current = checkpoint; - } - const currentCheckpoint = checkpoint; - for (const source of sources) { - const existing = currentCheckpoint.families.get(source.choiceId); - if (existing && (existing.source.resourceType !== source.resourceType - || existing.source.bindingId !== source.bindingId || existing.source.sourcePath !== source.sourcePath)) { - throw new Error('The direct coded source changed while it was loading. Reload the table and retry.'); - } - if (!existing) currentCheckpoint.families.set(source.choiceId, { - source, entries: [], seenCursors: new Set(), started: false, exhausted: false, - }); - } - const makeView = (): GroupCodedInventoryView => { - const families = [...currentCheckpoint.families.values()]; - return { - scopeKey: inventoryScopeKey, - sourceKey, - entries: families.flatMap((family) => family.entries), - hasMore: families.some((family) => Boolean(family.cursor)), - complete: sourceDiscovery?.complete === true && !sourceDiscovery.truncated && families.every((family) => family.exhausted), - }; - }; - const publish = () => setInventoryProgress(makeView()); - const fetchPage = async (family: GroupCodedInventoryFamily) => { - if (signal.aborted || family.exhausted) return; - const response = await client.browseSemanticInventory({ - project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot, - sourceChoiceId: family.source.choiceId, - ...(family.cursor ? { cursor: family.cursor } : {}), - limit: 50, - }, signal); - if (signal.aborted) return; - if (response.state !== 'complete') { - throw new Error(`Coded values for ${family.source.title} are not confirmed by a complete inventory. Wait for the catalog to be ready, then reopen the editor.`); - } - if (currentCheckpoint.contextToken !== response.contextToken - || currentCheckpoint.buildId !== response.buildId) { - throw new Error('The coded-value inventory changed while it was loading. Reload the table and retry.'); - } - family.entries.push(...response.entries.filter((item) => item.resourceType === family.source.resourceType - && item.bindingId === family.source.bindingId && item.sourcePath === family.source.sourcePath)); - family.cursor = response.nextCursor; - family.exhausted = !response.nextCursor; - family.started = true; - if (response.nextCursor) { - if (family.seenCursors.has(response.nextCursor)) { - throw new Error(`Coded values for ${family.source.title} could not be completely loaded. Reopen the editor to retry.`); - } - family.seenCursors.add(response.nextCursor); - } - }; - const unstarted = [...currentCheckpoint.families.values()].filter((family) => !family.started); - if (unstarted.length > 0) { - await Promise.all(unstarted.map(fetchPage)); - if (signal.aborted) return makeView(); - publish(); - } - if (!currentCheckpoint.initialized || unstarted.length > 0) { - while (!signal.aborted) { - const visibleKeys = new Set(makeView().entries.flatMap((item) => { - const entry = availableGroupCodedEntry(item, sources); - return entry ? [codedCategoryKey(entry.familyKey, item.system, item.code)] : []; - })); - if (visibleKeys.size >= INITIAL_VISIBLE_CODED_CHOICES) break; - const pending = [...currentCheckpoint.families.values()].filter((family) => !family.exhausted); - if (pending.length === 0) break; - await Promise.all(pending.map(fetchPage)); - if (signal.aborted) return makeView(); - publish(); - } - if (signal.aborted) return makeView(); - currentCheckpoint.initialized = true; - currentCheckpoint.loadedRound = pageRound; - } else if (pageRound > currentCheckpoint.loadedRound) { - for (let round = currentCheckpoint.loadedRound; round < pageRound && !signal.aborted; round += 1) { - const pending = [...currentCheckpoint.families.values()].filter((family) => Boolean(family.cursor)); - if (pending.length === 0) break; - await Promise.all(pending.map(fetchPage)); - if (signal.aborted) return makeView(); - currentCheckpoint.loadedRound = round + 1; - publish(); - } - } - return makeView(); - }, [client, inventoryScopeKey, readiness?.contextToken, readiness?.buildId, sourceKey, sourceDiscovery?.complete, sourceDiscovery?.truncated, sourceDiscovery?.loading, pageRound], - inventoryReady && ((sourceDiscovery !== undefined && sources.length > 0) || sourceDiscovery?.complete === true)); - - const inventoryView = inventoryQuery.data?.scopeKey === inventoryScopeKey && inventoryQuery.data.sourceKey === sourceKey - ? inventoryQuery.data - : inventoryProgress?.scopeKey === inventoryScopeKey && inventoryProgress.sourceKey === sourceKey ? inventoryProgress : undefined; - const inventory = inventoryView?.entries ?? []; - const available = useMemo(() => { - const entries = new Map>>(); - for (const item of inventory) { - const entry = availableGroupCodedEntry(item, sources); - if (entry) entries.set(codedCategoryKey(entry.familyKey, item.system, item.code), entry); - } - return [...entries.values()]; - }, [inventory, sources]); - - const savedCategoryKey = props.form.codedCategories.filter((category) => category.choice === 'saved') - .map((category) => category.key).join('\u0001'); - const savedCategoryQuery = useQuery(async (signal) => { - const unresolved = currentForm.current.codedCategories.filter((category) => category.choice === 'saved'); - const hydrated = await Promise.all(unresolved.map(async (category) => { - const source = sources.find((candidate) => candidate.resourceType === rowRoot - && codedFamilyKey(candidate.bindingId, candidate.sourcePath) === category.familyKey); - if (!source) return { key: category.key, value: undefined, pending: !sourceDiscovery?.complete }; - try { - const response = await client.browseSemanticInventory({ - project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot, - sourceChoiceId: source.choiceId, query: category.code, limit: 50, - }, signal); - if (response.state !== 'complete') { - throw new Error('The saved coded value is not confirmed by a complete inventory. Wait for the catalog to be ready, then reopen the editor.'); - } - if (response.contextToken !== readiness?.contextToken || response.buildId !== readiness?.buildId) { - throw new Error('The coded-value inventory changed while it was loading. Reload the table and retry.'); - } - const item = response.entries.find((candidate) => candidate.system === category.system && candidate.code === category.code - && availableGroupCodedEntry(candidate, [source])); - return { key: category.key, value: item?.constructionChoice ? { - sourceChoiceId: source.choiceId, - categoryChoiceId: item.constructionChoice.choiceId, - display: semanticConceptLabel(item), - valueType: item.valueType, - } : undefined, pending: false }; - } catch (reason: unknown) { - return { - key: category.key, - value: undefined, - pending: false, - error: reason instanceof Error ? reason.message : 'A saved coded value could not be reauthorized.', - }; - } - })); - if (signal.aborted || currentInventoryScopeKey.current !== inventoryScopeKey) return { unavailableKeys: [], error: undefined }; - const byKey = new Map(hydrated.flatMap((item) => item.value ? [[item.key, item.value] as const] : [])); - const form = currentForm.current; - const savedKeys = new Set(form.codedCategories.filter((category) => category.choice === 'saved').map((category) => category.key)); - let changed = false; - const codedCategories = form.codedCategories.map((category): GroupCodedCategory => { - if (category.choice !== 'saved') return category; - const item = byKey.get(category.key); - if (!item) return category; - changed = true; - return { - ...category, - choice: 'ready', - sourceChoiceId: item.sourceChoiceId, - categoryChoiceId: item.categoryChoiceId, - display: item.display, - valueType: item.valueType, - }; - }); - if (changed) changeForm.current({ ...form, codedCategories }); - return { - unavailableKeys: hydrated.filter((item) => savedKeys.has(item.key) && !item.value && !item.pending).map((item) => item.key), - error: hydrated.find((item) => item.error)?.error, - }; - }, [client, inventoryScopeKey, sourceKey, sourceDiscovery?.complete, sourceDiscovery?.loading, savedCategoryKey], - inventoryReady && sourceDiscovery !== undefined && savedCategoryKey.length > 0); - const unavailableSavedKeys = new Set(savedCategoryQuery.data?.unavailableKeys ?? []); - - const selectedFamily = props.form.codedCategories[0]?.familyKey; - const removeSelectedCategory = (category: GroupCodedCategory) => { - props.onChange({ - ...props.form, - codedCategories: props.form.codedCategories.filter((candidate) => candidate.key !== category.key), - keys: props.form.keys.filter((groupKey) => groupKey.outputColumnId !== category.groupOutputColumnId), - }); - }; - const toggle = (entry: (typeof available)[number]) => { - const { item, source, familyKey } = entry; - const key = codedCategoryKey(familyKey, item.system, item.code); - const prior = props.form.codedCategories.find((category) => category.key === key); - if (prior) { - removeSelectedCategory(prior); - return; - } - if (selectedFamily && selectedFamily !== familyKey || props.form.codedCategories.length >= 50 || !item.constructionChoice) return; - const label = semanticConceptLabel(item); - const usedNames = getUsedGroupNames(props.form); - const groupOutputName = uniqueName(item.code, usedNames); - const helperOutputName = uniqueName(item.code, new Set([ - ...props.stage.columns.map((column) => column.name.toLowerCase()), - ...props.form.codedCategories.map((category) => category.helperOutputName.toLowerCase()), - ])); - const groupKey: GroupKey = { - inputColumnId: createOpaqueId('coded-group-input'), - outputColumnId: createOpaqueId('coded-group-output'), - name: groupOutputName, - label, - }; - const codedCategory: GroupCodedCategory = { - choice: 'ready', - key, - familyKey, - system: item.system, - code: item.code, - display: label, - valueType: item.valueType, - sourceChoiceId: source.choiceId, - categoryChoiceId: item.constructionChoice.choiceId, - helperOutputColumnId: groupKey.inputColumnId, - groupOutputColumnId: groupKey.outputColumnId, - groupOutputName: groupKey.name, - groupOutputLabel: groupKey.label, - helperOutputName, - helperOutputLabel: label, - }; - const codedCategories = [...props.form.codedCategories, codedCategory]; - const nextForm = { ...props.form, codedCategories, keys: [...props.form.keys, groupKey] }; - props.onChange({ - ...nextForm, - codedPassthroughs: ensureGroupCodedPassthroughs(nextForm, props.stage, [ - ...nextForm.keys.flatMap((key) => codedCategories.some((category) => category.helperOutputColumnId === key.inputColumnId) ? [] : [key.inputColumnId]), - ...nextForm.aggregates.flatMap((aggregate) => aggregate.operation === 'COUNT_ROWS' ? [] : [aggregate.inputColumnId]), - ...nextForm.rowValues.flatMap((value) => 'inputColumnId' in value ? [value.inputColumnId] : []), - ]), - }); - }; - const loadNextPage = () => { - if (!inventoryView?.hasMore || inventoryQuery.isLoading || pageRound !== requestedRoundRef.current) return; - requestedRoundRef.current = pageRound + 1; - setPageRound(pageRound + 1); - }; - const onChoicesScroll = (event: React.UIEvent) => { - const element = event.currentTarget; - if (element.scrollHeight - element.scrollTop - element.clientHeight <= 24) loadNextPage(); - }; - const sourceListIncomplete = Boolean(sourceDiscovery && !sourceDiscovery.loading && (!sourceDiscovery.complete || sourceDiscovery.truncated)); - const error = readinessQuery.error ?? sourcesQuery.error ?? inventoryQuery.error ?? savedCategoryQuery.data?.error; - const errorText = error instanceof Error ? error.message : error ? 'Coded values could not be loaded.' : undefined; - const choicesLoading = readinessQuery.isLoading || sourcesQuery.isLoading || inventoryQuery.isLoading || savedCategoryQuery.isLoading; - if (inventoryReady && !choicesLoading && !errorText && !sourceListIncomplete && inventoryView?.complete && available.length === 0 - && props.form.codedCategories.length === 0) return null; - - return ( -
-
-

Coded values

-

Choose coded values to group by.

-
- {readinessQuery.isLoading && !readiness ?

Checking coded-value availability…

: null} - {!readinessQuery.error && readiness && !inventoryReady ? ( -

- {!readiness.buildId - ? 'No current coded-value catalog is available.' - : `The coded-value catalog is ${readiness.state}. Choices will appear when it is complete.`} -

- ) : null} - {inventoryReady && choicesLoading ?

Loading coded values…

: null} - {errorText ?

{errorText}

: null} - {sourceListIncomplete && available.length > 0 ?

The direct source-field list may be incomplete; additional coded choices may exist.

: null} - {selectedFamily && props.form.codedCategories.some((category) => category.familyKey !== selectedFamily) ? ( -

These selected concepts come from different source fields. A group can use coded values from one direct source field at a time; remove the conflicting selection to continue.

- ) : selectedFamily ? ( -

Selected source field: {sources.find((source) => codedFamilyKey(source.bindingId, source.sourcePath) === selectedFamily)?.title ?? 'saved direct source'}. Choose coded values from this source field.

- ) : null} -
- {sources.map((source) => { - const sourceEntries = available.filter((entry) => entry.source.choiceId === source.choiceId); - if (sourceEntries.length === 0 && !inventoryView?.complete) return null; - return ( -
-
{source.title}
- {sourceEntries.map((entry) => { - const { item, familyKey } = entry; - const key = codedCategoryKey(familyKey, item.system, item.code); - const selected = props.form.codedCategories.some((category) => category.key === key); - const familyConflict = Boolean(selectedFamily && selectedFamily !== familyKey); - return ( - - ); - })} -
- ); - })} - {props.form.codedCategories.filter((category) => !available.some((entry) => - category.key === codedCategoryKey(entry.familyKey, entry.item.system, entry.item.code))).map((category) => { - const familyConflict = Boolean(selectedFamily && selectedFamily !== category.familyKey); - return ( - - ); - })} - {inventoryView?.hasMore ?

Scroll for more choices from these source fields.

: null} -
- {inventoryReady && !choicesLoading && !errorText && available.length === 0 ? ( -

- {sourceListIncomplete - ? 'No ready coded values were found among the listed direct source fields; the source list may be incomplete.' - : inventoryView?.complete - ? 'No ready coded values are available from these direct source fields.' - : 'Coded values from direct source fields are still loading.'} -

- ) : null} - {props.form.codedCategories.length > 0 ?

{props.form.codedCategories.length} coded value{props.form.codedCategories.length === 1 ? '' : 's'} selected.

: null} -
- ); -}; - const groupAggregateKindFromInput = (value: string): GroupAggregateKind | undefined => { switch (value) { case 'COUNT_ROWS': return 'COUNT_ROWS'; diff --git a/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx b/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx new file mode 100644 index 000000000..f42c2f380 --- /dev/null +++ b/ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx @@ -0,0 +1,534 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { useQuery } from '../../../react'; +import type { + FrameSourceOption, + SemanticInventoryBrowseResponse, + SemanticInventoryItem, +} from '../../../types'; +import { semanticConceptLabel } from '../catalogItems'; +import { + codedCategoryKey, + codedFamilyKey, + createOpaqueId, + ensureGroupCodedPassthroughs, + getUsedGroupNames, + uniqueName, + type ConstructionReshapeEditorProps, + type GroupCodedCategory, + type GroupForm, + type GroupKey, + type ReshapeStage, +} from './ConstructionReshapeEditor'; + +const INITIAL_VISIBLE_CODED_CHOICES = 10; +const CODED_INVENTORY_READINESS_POLL_INTERVAL_MS = 1_000; + +const waitForCodedInventoryReadinessPoll = (signal: AbortSignal): Promise => new Promise((resolve) => { + if (signal.aborted) { + resolve(); + return; + } + let timer: number | undefined; + const finish = () => { + if (timer !== undefined) window.clearTimeout(timer); + signal.removeEventListener('abort', finish); + resolve(); + }; + timer = window.setTimeout(finish, CODED_INVENTORY_READINESS_POLL_INTERVAL_MS); + signal.addEventListener('abort', finish, { once: true }); +}); + +const availableGroupCodedEntry = ( + item: SemanticInventoryItem, + sources: ReadonlyArray, +) => { + const source = sources.find((candidate) => candidate.resourceType === item.resourceType + && candidate.bindingId === item.bindingId && candidate.sourcePath === item.sourcePath); + const semanticSource = item.constructionChoice?.source; + if (!source || !item.code || item.codingVersion || !item.constructionChoice || item.constructionChoice.route.length !== 0 + || semanticSource?.kind !== 'SEMANTIC' || semanticSource.resourceType !== source.resourceType + || semanticSource.bindingId !== source.bindingId || semanticSource.sourcePath !== source.sourcePath + || semanticSource.code !== item.code || (semanticSource.system ?? '') !== item.system + || item.readiness.status !== 'READY' && item.readiness.status !== 'READY_WITH_WARNING') return undefined; + return { item, source, familyKey: codedFamilyKey(source.bindingId, source.sourcePath) }; +}; + +type GroupCodedSourceDiscovery = { + readonly scopeKey: string; + readonly sources: ReadonlyArray; + readonly complete: boolean; + readonly truncated: boolean; + readonly loading: boolean; +}; + +type GroupCodedInventoryReadiness = { + readonly scopeKey: string; + readonly contextToken: string; + readonly buildId: string; + readonly state: SemanticInventoryBrowseResponse['state']; +}; + +type GroupCodedInventoryFamily = { + readonly source: FrameSourceOption; + readonly entries: SemanticInventoryItem[]; + readonly seenCursors: Set; + started: boolean; + cursor?: string; + exhausted: boolean; +}; + +type GroupCodedInventoryCheckpoint = { + readonly scopeKey: string; + readonly families: Map; + contextToken?: string; + buildId?: string; + initialized: boolean; + loadedRound: number; +}; + +type GroupCodedInventoryView = { + readonly scopeKey: string; + readonly sourceKey: string; + readonly entries: ReadonlyArray; + readonly hasMore: boolean; + readonly complete: boolean; +}; + +export type GroupCodedValuePickerProps = { + readonly context: NonNullable; + readonly form: GroupForm; + readonly stage: ReshapeStage; + readonly disabled: boolean; + readonly onChange: (form: GroupForm) => void; +}; + +export const GroupCodedValuePicker = (props: GroupCodedValuePickerProps) => { + const { client, project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot } = props.context; + const [pageRound, setPageRound] = useState(0); + const [sourceProgress, setSourceProgress] = useState(); + const [inventoryProgress, setInventoryProgress] = useState(); + const [readinessProgress, setReadinessProgress] = useState(); + const checkpointRef = useRef(undefined); + const requestedRoundRef = useRef(0); + const currentForm = useRef(props.form); + currentForm.current = props.form; + const changeForm = useRef(props.onChange); + changeForm.current = props.onChange; + + const sourceScopeKey = JSON.stringify([project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot]); + const readinessQuery = useQuery(async (signal) => { + const browseReadiness = () => client.browseSemanticInventory({ + project, explorerId, authResourcePath, snapshotToken, rowRoot, limit: 1, + }, signal); + let response = await browseReadiness(); + while (response.state === 'running' && !signal.aborted) { + setReadinessProgress({ + scopeKey: sourceScopeKey, + contextToken: response.contextToken, + buildId: response.buildId, + state: response.state, + }); + await waitForCodedInventoryReadinessPoll(signal); + if (!signal.aborted) response = await browseReadiness(); + } + return { + scopeKey: sourceScopeKey, + contextToken: response.contextToken, + buildId: response.buildId, + state: response.state, + } satisfies GroupCodedInventoryReadiness; + }, [client, sourceScopeKey]); + const readiness = readinessQuery.data?.scopeKey === sourceScopeKey + ? readinessQuery.data + : readinessProgress?.scopeKey === sourceScopeKey ? readinessProgress : undefined; + const inventoryReady = Boolean(readiness?.buildId && readiness.state === 'complete'); + const inventoryScopeKey = JSON.stringify([sourceScopeKey, readiness?.contextToken, readiness?.buildId]); + const sourcesQuery = useQuery(async (signal) => { + const sources = new Map(); + const seenCursors = new Set(); + let cursor: string | undefined; + let complete = false; + let truncated = false; + do { + const response = await client.browseFrameSourceOptions({ + project, explorerId, authResourcePath, snapshotToken, outputId, resourceType: rowRoot, limit: 50, + ...(cursor ? { cursor } : {}), + }, signal); + if (response.snapshotToken !== snapshotToken || response.outputId !== outputId) { + throw new Error('Coded source fields were returned for a different table snapshot. Reload the table and retry.'); + } + for (const source of response.sources) { + if (source.route.length === 0 && source.resourceType === rowRoot) sources.set(source.choiceId, source); + } + complete = response.complete; + truncated = truncated || response.truncated; + cursor = response.nextCursor; + const progress = { + scopeKey: inventoryScopeKey, + sources: [...sources.values()], + complete: complete && !cursor, + truncated, + loading: Boolean(cursor), + } satisfies GroupCodedSourceDiscovery; + if (!signal.aborted) setSourceProgress(progress); + if (cursor && seenCursors.has(cursor)) throw new Error('Coded source fields could not be completely listed. Reopen the editor to retry.'); + if (cursor) seenCursors.add(cursor); + } while (cursor && !signal.aborted); + return { + scopeKey: inventoryScopeKey, + sources: [...sources.values()], + complete: complete && !cursor, + truncated, + loading: false, + } satisfies GroupCodedSourceDiscovery; + }, [client, inventoryScopeKey], inventoryReady); + + const sourceDiscovery = sourcesQuery.data?.scopeKey === inventoryScopeKey + ? sourcesQuery.data + : sourceProgress?.scopeKey === inventoryScopeKey ? sourceProgress : undefined; + const sources = sourceDiscovery?.sources ?? []; + const sourceKey = JSON.stringify(sources.map((source) => [source.choiceId, source.bindingId, source.sourcePath])); + const currentInventoryScopeKey = useRef(inventoryScopeKey); + currentInventoryScopeKey.current = inventoryScopeKey; + const inventoryQuery = useQuery(async (signal) => { + let checkpoint = checkpointRef.current; + if (!checkpoint || checkpoint.scopeKey !== inventoryScopeKey + || checkpoint.contextToken !== readiness?.contextToken || checkpoint.buildId !== readiness?.buildId) { + checkpoint = { + scopeKey: inventoryScopeKey, + families: new Map(), + contextToken: readiness?.contextToken, + buildId: readiness?.buildId, + initialized: false, + loadedRound: 0, + }; + checkpointRef.current = checkpoint; + } + const currentCheckpoint = checkpoint; + for (const source of sources) { + const existing = currentCheckpoint.families.get(source.choiceId); + if (existing && (existing.source.resourceType !== source.resourceType + || existing.source.bindingId !== source.bindingId || existing.source.sourcePath !== source.sourcePath)) { + throw new Error('The direct coded source changed while it was loading. Reload the table and retry.'); + } + if (!existing) currentCheckpoint.families.set(source.choiceId, { + source, entries: [], seenCursors: new Set(), started: false, exhausted: false, + }); + } + const makeView = (): GroupCodedInventoryView => { + const families = [...currentCheckpoint.families.values()]; + return { + scopeKey: inventoryScopeKey, + sourceKey, + entries: families.flatMap((family) => family.entries), + hasMore: families.some((family) => Boolean(family.cursor)), + complete: sourceDiscovery?.complete === true && !sourceDiscovery.truncated && families.every((family) => family.exhausted), + }; + }; + const publish = () => setInventoryProgress(makeView()); + const fetchPage = async (family: GroupCodedInventoryFamily) => { + if (signal.aborted || family.exhausted) return; + const response = await client.browseSemanticInventory({ + project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot, + sourceChoiceId: family.source.choiceId, + ...(family.cursor ? { cursor: family.cursor } : {}), + limit: 50, + }, signal); + if (signal.aborted) return; + if (response.state !== 'complete') { + throw new Error(`Coded values for ${family.source.title} are not confirmed by a complete inventory. Wait for the catalog to be ready, then reopen the editor.`); + } + if (currentCheckpoint.contextToken !== response.contextToken + || currentCheckpoint.buildId !== response.buildId) { + throw new Error('The coded-value inventory changed while it was loading. Reload the table and retry.'); + } + family.entries.push(...response.entries.filter((item) => item.resourceType === family.source.resourceType + && item.bindingId === family.source.bindingId && item.sourcePath === family.source.sourcePath)); + family.cursor = response.nextCursor; + family.exhausted = !response.nextCursor; + family.started = true; + if (response.nextCursor) { + if (family.seenCursors.has(response.nextCursor)) { + throw new Error(`Coded values for ${family.source.title} could not be completely loaded. Reopen the editor to retry.`); + } + family.seenCursors.add(response.nextCursor); + } + }; + const unstarted = [...currentCheckpoint.families.values()].filter((family) => !family.started); + if (unstarted.length > 0) { + await Promise.all(unstarted.map(fetchPage)); + if (signal.aborted) return makeView(); + publish(); + } + if (!currentCheckpoint.initialized || unstarted.length > 0) { + while (!signal.aborted) { + const visibleKeys = new Set(makeView().entries.flatMap((item) => { + const entry = availableGroupCodedEntry(item, sources); + return entry ? [codedCategoryKey(entry.familyKey, item.system, item.code)] : []; + })); + if (visibleKeys.size >= INITIAL_VISIBLE_CODED_CHOICES) break; + const pending = [...currentCheckpoint.families.values()].filter((family) => !family.exhausted); + if (pending.length === 0) break; + await Promise.all(pending.map(fetchPage)); + if (signal.aborted) return makeView(); + publish(); + } + if (signal.aborted) return makeView(); + currentCheckpoint.initialized = true; + currentCheckpoint.loadedRound = pageRound; + } else if (pageRound > currentCheckpoint.loadedRound) { + for (let round = currentCheckpoint.loadedRound; round < pageRound && !signal.aborted; round += 1) { + const pending = [...currentCheckpoint.families.values()].filter((family) => Boolean(family.cursor)); + if (pending.length === 0) break; + await Promise.all(pending.map(fetchPage)); + if (signal.aborted) return makeView(); + currentCheckpoint.loadedRound = round + 1; + publish(); + } + } + return makeView(); + }, [client, inventoryScopeKey, readiness?.contextToken, readiness?.buildId, sourceKey, sourceDiscovery?.complete, sourceDiscovery?.truncated, sourceDiscovery?.loading, pageRound], + inventoryReady && ((sourceDiscovery !== undefined && sources.length > 0) || sourceDiscovery?.complete === true)); + + const inventoryView = inventoryQuery.data?.scopeKey === inventoryScopeKey && inventoryQuery.data.sourceKey === sourceKey + ? inventoryQuery.data + : inventoryProgress?.scopeKey === inventoryScopeKey && inventoryProgress.sourceKey === sourceKey ? inventoryProgress : undefined; + const inventory = inventoryView?.entries ?? []; + const available = useMemo(() => { + const entries = new Map>>(); + for (const item of inventory) { + const entry = availableGroupCodedEntry(item, sources); + if (entry) entries.set(codedCategoryKey(entry.familyKey, item.system, item.code), entry); + } + return [...entries.values()]; + }, [inventory, sources]); + + const savedCategoryKey = props.form.codedCategories.filter((category) => category.choice === 'saved') + .map((category) => category.key).join('\u0001'); + const savedCategoryQuery = useQuery(async (signal) => { + const unresolved = currentForm.current.codedCategories.filter((category) => category.choice === 'saved'); + const hydrated = await Promise.all(unresolved.map(async (category) => { + const source = sources.find((candidate) => candidate.resourceType === rowRoot + && codedFamilyKey(candidate.bindingId, candidate.sourcePath) === category.familyKey); + if (!source) return { key: category.key, value: undefined, pending: !sourceDiscovery?.complete }; + try { + const response = await client.browseSemanticInventory({ + project, explorerId, authResourcePath, snapshotToken, outputId, rowRoot, + sourceChoiceId: source.choiceId, query: category.code, limit: 50, + }, signal); + if (response.state !== 'complete') { + throw new Error('The saved coded value is not confirmed by a complete inventory. Wait for the catalog to be ready, then reopen the editor.'); + } + if (response.contextToken !== readiness?.contextToken || response.buildId !== readiness?.buildId) { + throw new Error('The coded-value inventory changed while it was loading. Reload the table and retry.'); + } + const item = response.entries.find((candidate) => candidate.system === category.system && candidate.code === category.code + && availableGroupCodedEntry(candidate, [source])); + return { key: category.key, value: item?.constructionChoice ? { + sourceChoiceId: source.choiceId, + categoryChoiceId: item.constructionChoice.choiceId, + display: semanticConceptLabel(item), + valueType: item.valueType, + } : undefined, pending: false }; + } catch (reason: unknown) { + return { + key: category.key, + value: undefined, + pending: false, + error: reason instanceof Error ? reason.message : 'A saved coded value could not be reauthorized.', + }; + } + })); + if (signal.aborted || currentInventoryScopeKey.current !== inventoryScopeKey) return { unavailableKeys: [], error: undefined }; + const byKey = new Map(hydrated.flatMap((item) => item.value ? [[item.key, item.value] as const] : [])); + const form = currentForm.current; + const savedKeys = new Set(form.codedCategories.filter((category) => category.choice === 'saved').map((category) => category.key)); + let changed = false; + const codedCategories = form.codedCategories.map((category): GroupCodedCategory => { + if (category.choice !== 'saved') return category; + const item = byKey.get(category.key); + if (!item) return category; + changed = true; + return { + ...category, + choice: 'ready', + sourceChoiceId: item.sourceChoiceId, + categoryChoiceId: item.categoryChoiceId, + display: item.display, + valueType: item.valueType, + }; + }); + if (changed) changeForm.current({ ...form, codedCategories }); + return { + unavailableKeys: hydrated.filter((item) => savedKeys.has(item.key) && !item.value && !item.pending).map((item) => item.key), + error: hydrated.find((item) => item.error)?.error, + }; + }, [client, inventoryScopeKey, sourceKey, sourceDiscovery?.complete, sourceDiscovery?.loading, savedCategoryKey], + inventoryReady && sourceDiscovery !== undefined && savedCategoryKey.length > 0); + const unavailableSavedKeys = new Set(savedCategoryQuery.data?.unavailableKeys ?? []); + + const selectedFamily = props.form.codedCategories[0]?.familyKey; + const removeSelectedCategory = (category: GroupCodedCategory) => { + props.onChange({ + ...props.form, + codedCategories: props.form.codedCategories.filter((candidate) => candidate.key !== category.key), + keys: props.form.keys.filter((groupKey) => groupKey.outputColumnId !== category.groupOutputColumnId), + }); + }; + const toggle = (entry: (typeof available)[number]) => { + const { item, source, familyKey } = entry; + const key = codedCategoryKey(familyKey, item.system, item.code); + const prior = props.form.codedCategories.find((category) => category.key === key); + if (prior) { + removeSelectedCategory(prior); + return; + } + if (selectedFamily && selectedFamily !== familyKey || props.form.codedCategories.length >= 50 || !item.constructionChoice) return; + const label = semanticConceptLabel(item); + const usedNames = getUsedGroupNames(props.form); + const groupOutputName = uniqueName(item.code, usedNames); + const helperOutputName = uniqueName(item.code, new Set([ + ...props.stage.columns.map((column) => column.name.toLowerCase()), + ...props.form.codedCategories.map((category) => category.helperOutputName.toLowerCase()), + ])); + const groupKey: GroupKey = { + inputColumnId: createOpaqueId('coded-group-input'), + outputColumnId: createOpaqueId('coded-group-output'), + name: groupOutputName, + label, + }; + const codedCategory: GroupCodedCategory = { + choice: 'ready', + key, + familyKey, + system: item.system, + code: item.code, + display: label, + valueType: item.valueType, + sourceChoiceId: source.choiceId, + categoryChoiceId: item.constructionChoice.choiceId, + helperOutputColumnId: groupKey.inputColumnId, + groupOutputColumnId: groupKey.outputColumnId, + groupOutputName: groupKey.name, + groupOutputLabel: groupKey.label, + helperOutputName, + helperOutputLabel: label, + }; + const codedCategories = [...props.form.codedCategories, codedCategory]; + const nextForm = { ...props.form, codedCategories, keys: [...props.form.keys, groupKey] }; + props.onChange({ + ...nextForm, + codedPassthroughs: ensureGroupCodedPassthroughs(nextForm, props.stage, [ + ...nextForm.keys.flatMap((key) => codedCategories.some((category) => category.helperOutputColumnId === key.inputColumnId) ? [] : [key.inputColumnId]), + ...nextForm.aggregates.flatMap((aggregate) => aggregate.operation === 'COUNT_ROWS' ? [] : [aggregate.inputColumnId]), + ...nextForm.rowValues.flatMap((value) => 'inputColumnId' in value ? [value.inputColumnId] : []), + ]), + }); + }; + const loadNextPage = () => { + if (!inventoryView?.hasMore || inventoryQuery.isLoading || pageRound !== requestedRoundRef.current) return; + requestedRoundRef.current = pageRound + 1; + setPageRound(pageRound + 1); + }; + const onChoicesScroll = (event: React.UIEvent) => { + const element = event.currentTarget; + if (element.scrollHeight - element.scrollTop - element.clientHeight <= 24) loadNextPage(); + }; + const sourceListIncomplete = Boolean(sourceDiscovery && !sourceDiscovery.loading && (!sourceDiscovery.complete || sourceDiscovery.truncated)); + const error = readinessQuery.error ?? sourcesQuery.error ?? inventoryQuery.error ?? savedCategoryQuery.data?.error; + const errorText = error instanceof Error ? error.message : error ? 'Coded values could not be loaded.' : undefined; + const choicesLoading = readinessQuery.isLoading || sourcesQuery.isLoading || inventoryQuery.isLoading || savedCategoryQuery.isLoading; + if (inventoryReady && !choicesLoading && !errorText && !sourceListIncomplete && inventoryView?.complete && available.length === 0 + && props.form.codedCategories.length === 0) return null; + + return ( +
+
+

Coded values

+

Choose coded values to group by.

+
+ {readinessQuery.isLoading && !readiness ?

Checking coded-value availability…

: null} + {!readinessQuery.error && readiness && !inventoryReady ? ( +

+ {!readiness.buildId + ? 'No current coded-value catalog is available.' + : `The coded-value catalog is ${readiness.state}. Choices will appear when it is complete.`} +

+ ) : null} + {inventoryReady && choicesLoading ?

Loading coded values…

: null} + {errorText ?

{errorText}

: null} + {sourceListIncomplete && available.length > 0 ?

The direct source-field list may be incomplete; additional coded choices may exist.

: null} + {selectedFamily && props.form.codedCategories.some((category) => category.familyKey !== selectedFamily) ? ( +

These selected concepts come from different source fields. A group can use coded values from one direct source field at a time; remove the conflicting selection to continue.

+ ) : selectedFamily ? ( +

Selected source field: {sources.find((source) => codedFamilyKey(source.bindingId, source.sourcePath) === selectedFamily)?.title ?? 'saved direct source'}. Choose coded values from this source field.

+ ) : null} +
+ {sources.map((source) => { + const sourceEntries = available.filter((entry) => entry.source.choiceId === source.choiceId); + if (sourceEntries.length === 0 && !inventoryView?.complete) return null; + return ( +
+
{source.title}
+ {sourceEntries.map((entry) => { + const { item, familyKey } = entry; + const key = codedCategoryKey(familyKey, item.system, item.code); + const selected = props.form.codedCategories.some((category) => category.key === key); + const familyConflict = Boolean(selectedFamily && selectedFamily !== familyKey); + return ( + + ); + })} +
+ ); + })} + {props.form.codedCategories.filter((category) => !available.some((entry) => + category.key === codedCategoryKey(entry.familyKey, entry.item.system, entry.item.code))).map((category) => { + const familyConflict = Boolean(selectedFamily && selectedFamily !== category.familyKey); + return ( + + ); + })} + {inventoryView?.hasMore ?

Scroll for more choices from these source fields.

: null} +
+ {inventoryReady && !choicesLoading && !errorText && available.length === 0 ? ( +

+ {sourceListIncomplete + ? 'No ready coded values were found among the listed direct source fields; the source list may be incomplete.' + : inventoryView?.complete + ? 'No ready coded values are available from these direct source fields.' + : 'Coded values from direct source fields are still loading.'} +

+ ) : null} + {props.form.codedCategories.length > 0 ?

{props.form.codedCategories.length} coded value{props.form.codedCategories.length === 1 ? '' : 's'} selected.

: null} +
+ ); +}; From 3f94a4dfafdaaa0cc7943923358d65037b8e178d Mon Sep 17 00:00:00 2001 From: Matthew Peterkort <33436238+matthewpeterkort@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:29:50 +0000 Subject: [PATCH 2/2] chore(verification): re-pin UI contract inventory and reviewed sources for the picker extraction Moving functions means updating explicit source maps, not neutering checks (repo-simplify-199 follow-up, coordinator-authorized shared-fixture commit). - verification/builder-interactions/reviewed-sources.json: re-pin the ConstructionReshapeEditor.tsx entry to the digest recomputed from the actual blob at this head (067873e6...), verified with the same sha256 digest() the formal verifier uses; all 29 reviewed entries re-verified, 0 failures. - docs/ui-contract-inventory/relational.fragment.json: re-cite all 99 distinct ConstructionReshapeEditor.tsx line references for the picker extraction, applied by a rerunnable tool that byte-verifies every mapped slice or refuses: 88 in-parent shifts (byte-identical), 10 picker-region citations moved to GroupCodedValuePicker.tsx (byte-identical), and the single straddling reference 554-660 split into GroupCodedValuePicker.tsx: 78-95 (helpers) plus ConstructionReshapeEditor.tsx:496-583, whose first line carries only the deliberate `export ` prefix added to uniqueName. - docs/ui-contract-inventory/ui-supported-affordances.json: regenerated by the repo's own canonicalizer (scripts/normalize_ui_contract_inventory.py): relational fragment sha256, expandedSha256, and sourceCoverage.watchedFiles (ConstructionReshapeEditor hash re-pinned, GroupCodedValuePicker.tsx added at its actual-bytes digest). Pristine-base regeneration was confirmed byte-identical to the previously committed manifest before editing, and lane 259's PR 204 canonicalizer reproduces this exact manifest bytes over the same tree, so the pinned expandedSha256 does not churn under their simplification. Scope discipline: only ConstructionReshapeEditor/GroupCodedValuePicker entries were touched; no unrelated re-pins. No checks removed or weakened. Verification: - verify_ui_contract_inventory.py --source-only: PASS (CI gate mode) - verify_ui_contract_inventory.py --self-test: both mutation probes PASS, including the builder.reshape.group_coded omission probe proving the re-cited picker evidence still binds CODED_GROUP - verify_ui_contract_inventory.py full mode: issue set byte-identical to the pristine 80e88771 baseline (399 pre-existing pending-coverage rows, zero new issues) - scripts/tests/test_normalize_ui_contract_inventory.py: 5/5 OK - reviewed-sources digest loop (formal-tools digest()): 29/29 match - ConstructionReshapeEditor.unit.test.tsx: 103/103 passed, 0 skipped - git diff --check: clean --- .../relational.fragment.json | 263 +++++++++--------- .../ui-supported-affordances.json | 10 +- .../reviewed-sources.json | 2 +- 3 files changed, 140 insertions(+), 135 deletions(-) diff --git a/docs/ui-contract-inventory/relational.fragment.json b/docs/ui-contract-inventory/relational.fragment.json index 16cf08f98..02ba0f81c 100644 --- a/docs/ui-contract-inventory/relational.fragment.json +++ b/docs/ui-contract-inventory/relational.fragment.json @@ -47,37 +47,37 @@ "basis": "backend-promise", "statement": "The UI describes one output row per unique combination of chosen group keys, with summary columns.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2274-2306" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2197-2229" ] }, { "basis": "observed-ui", "statement": "The client candidate builder does not enforce a non-empty key list, but requires at least one summary and valid unique output names.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:979-999" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:902-922" ] }, { "basis": "observed-ui", "statement": "COUNT_ROWS needs no field input; other summary kinds use compatible stage columns.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3311-3392" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3234-3315" ] }, { "basis": "backend-promise", "statement": "The selected missing-key policy is presented as running before summaries: GROUP combines absent/null keys, EXCLUDE removes rows missing any key, ERROR stops.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3301-3308", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3408-3427" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3224-3231", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3331-3350" ] }, { "basis": "observed-ui", "statement": "Related field choices are disabled when selected fields conflict across routes; coding identity cannot combine with semantic/coded keys.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:988-992", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3123-3168" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:911-915", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3046-3091" ] } ], @@ -86,14 +86,15 @@ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3251-3309", "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3476-3500", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/RowDefinitionSettingsPanel.tsx:872-897", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2270-2327", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:554-660", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:979-1022", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1962-1979", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3311-3392", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3404-3427", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2740-2782", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3842-3891", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2193-2250", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:78-95", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:496-583", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:902-945", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1885-1902", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3234-3315", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3327-3350", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2663-2705", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:213-262", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionWorkspace/useConstructionLifecycle.ts:233-252", "ui/packages/loom-ui/src/api.ts:1674-1678" ] @@ -115,24 +116,24 @@ "basis": "observed-ui", "statement": "Every chosen source field becomes a group key; the default summary counts source records.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1082-1146" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1005-1069" ] }, { "basis": "observed-ui", "statement": "A source-field group can only be proposed from the source projection stage.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1127-1146", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1962-1971" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1050-1069", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1885-1894" ] } ], "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3476-3483", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2049-2066", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1082-1146", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1962-1971", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2308-2327" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1972-1989", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1005-1069", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1885-1894", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2231-2250" ], "semantic_domains": [ { @@ -152,8 +153,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2049-2066", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1082-1119" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1972-1989", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1005-1042" ] }, { @@ -172,8 +173,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1102-1110", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1127-1145" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1025-1033", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1050-1068" ] }, { @@ -191,7 +192,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1111-1119" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1034-1042" ] } ] @@ -227,9 +228,9 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2970-2975", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3051-3072", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3123-3169" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2893-2898", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2974-2995", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3046-3092" ] }, { @@ -248,7 +249,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3046-3072" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2969-2995" ] }, { @@ -267,7 +268,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:988-990" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:911-913" ] }, { @@ -289,8 +290,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3311-3341", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:985-998" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3234-3264", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:908-921" ] }, { @@ -310,7 +311,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3404-3427" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3327-3350" ] }, { @@ -333,8 +334,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2740-2782", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2811-2831" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2663-2705", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2734-2754" ] }, { @@ -356,8 +357,8 @@ "maximum": 50 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3635-3654", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3883-3890" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3558-3577", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:254-261" ] }, { @@ -376,8 +377,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:255-261", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:988-998" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:251-257", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:911-921" ] } ], @@ -386,33 +387,33 @@ "basis": "backend-promise", "statement": "The UI describes one output row per unique combination of chosen group keys, with summary columns; a zero-key summary is not rejected by the client candidate builder.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2274-2306", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:979-999" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2197-2229", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:902-922" ] }, { "basis": "observed-ui", "statement": "The candidate builder requires at least one summary and validates unique physical output names; COUNT_ROWS has no field input, other summaries use compatible stage columns.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:985-998", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3311-3392" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:908-921", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3234-3315" ] }, { "basis": "backend-promise", "statement": "The selected missing-key policy is presented as running before summaries: GROUP combines absent/null keys, EXCLUDE removes rows missing any key, ERROR stops.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3301-3308", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3408-3427" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3224-3231", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3331-3350" ] }, { "basis": "observed-ui", "statement": "Group field choices using related occurrences are disabled when saved or selected fields conflict across routes; a coding-identity key cannot be combined with semantic/coded keys.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:988-992", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3123-3168", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3046-3072" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:911-915", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3046-3091", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2969-2995" ] } ] @@ -454,10 +455,10 @@ }, "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3483-3491", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:451-455", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3953-3968", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:447-451", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:324-339", "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3653-3665", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4182-4240" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3668-3726" ] }, { @@ -477,8 +478,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4035-4058", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4066-4071" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:406-429", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:437-442" ] }, { @@ -499,8 +500,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4000-4011", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:913-925" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:371-382", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:836-848" ] }, { @@ -522,8 +523,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4012-4034", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:683-692" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:383-405", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:606-615" ] }, { @@ -542,7 +543,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:663-692" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:586-615" ] } ], @@ -564,8 +565,8 @@ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3398-3450", "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3653-3665", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/RowDefinitionSettingsPanel.tsx:1011-1021", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2037-2039", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4182-4240" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1960-1962", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3668-3726" ] }, { @@ -583,25 +584,25 @@ "basis": "observed-ui", "statement": "The candidate requires exactly one selected list input and a selected empty-list policy; it requires valid unique output names.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:696-708", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1215-1227" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:619-631", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1138-1150" ] }, { "basis": "backend-promise", "statement": "The UI describes each list item as a row for its input record and repeats other fields. For empty or missing lists, ERROR stops, EXCLUDE drops the input row, or PRESERVE_PARENT keeps it with an empty item.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3986-3994", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3989-3998", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4035-4058" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:357-365", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:360-369", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:406-429" ] }, { "basis": "backend-promise", "statement": "Optional item position output is a zero-based integer column; other current-stage columns remain as passthrough outputs.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:663-694", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4012-4034" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:586-617", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:383-405" ] } ], @@ -611,10 +612,10 @@ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:4454-4456", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx:546-557", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/PreviewTable.tsx:726-735", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:663-708", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1215-1227", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3953-3968", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3986-4058", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:586-631", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1138-1150", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:324-339", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx:357-429", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionWorkspace/useConstructionLifecycle.ts:233-252", "ui/packages/loom-ui/src/api.ts:1674-1678" ] @@ -638,24 +639,24 @@ "basis": "backend-promise", "statement": "The result has one row per observed code and exposes system, version, code, and source-record count.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2714-2714", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1182-1206" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2637-2637", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1105-1129" ] }, { "basis": "observed-ui", "statement": "Applying the step replaces current columns with the coded-group outputs; incomplete coding-path selections cannot be proposed.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1180", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1182-1206" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1101-1103", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1105-1129" ] } ], "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3476-3478", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2308-2327", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1149-1206", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1977-1979" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2231-2250", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1072-1129", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1900-1902" ], "semantic_domains": [ { @@ -676,7 +677,7 @@ }, "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3476-3478", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1180" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1101-1103" ] }, { @@ -695,8 +696,8 @@ "maximum": 4 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1160-1170", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1206" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1083-1093", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1101-1129" ] }, { @@ -714,8 +715,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1160-1170", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2715-2734" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1083-1093", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2638-2657" ] } ], @@ -724,15 +725,15 @@ "basis": "observed-ui", "statement": "The form only creates a candidate when the selected codingPath matches a server choice and exactly four output columns are present.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1180" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1101-1103" ] }, { "basis": "backend-promise", "statement": "The UI describes the result as one row per observed code with system, version, code, and source-record count; applying replaces current columns with these outputs.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2714-2714", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1182-1206" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2637-2637", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1105-1129" ] } ], @@ -926,8 +927,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4244-4250", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4323-4348" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3730-3736", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3809-3834" ] }, { @@ -945,7 +946,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4349-4355" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3835-3841" ] }, { @@ -963,7 +964,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4357-4363" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3843-3849" ] }, { @@ -986,8 +987,8 @@ }, "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionWorkspace/useConstructionLifecycle.ts:377-425", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4375-4391", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1240-1245" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3861-3877", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1163-1168" ] }, { @@ -1008,7 +1009,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4454-4468" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3940-3954" ] }, { @@ -1025,7 +1026,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4470-4475" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3956-3961" ] }, { @@ -1047,7 +1048,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4477-4486" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3963-3972" ] }, { @@ -1065,8 +1066,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4242-4250", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1962-1989" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3728-3736", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1885-1912" ] } ], @@ -1075,9 +1076,9 @@ "basis": "observed-ui", "statement": "The Pivot editor owns an accessible Finding categories progress state for the current request identity during debounce and while its discovery owner has not published a matching terminal state or adopted result. It hides prior-pair categories, rejects superseded callbacks by revision/context/owner/stage identity, hides progress while disabled, and retains unrelated form choices across discovery failure and retry.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2130-2160", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2219-2234", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2363-2471", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2053-2083", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2142-2157", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2286-2394", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.unit.test.tsx:4475-4633" ] }, @@ -1085,24 +1086,24 @@ "basis": "observed-ui", "statement": "At least one group key and included category are required; category/value roles differ; every discovered category must be selected to form a new pivot candidate.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1240-1245", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4381-4389", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1985-1989" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1163-1168", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3867-3875", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1908-1912" ] }, { "basis": "backend-promise", "statement": "Each selected category becomes an output column while group columns identify output rows; duplicate, missing-cell, and unlisted-category behavior is explicit.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:821-833", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4454-4486" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:744-756", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3940-3972" ] }, { "basis": "observed-ui", "statement": "Only 50 matching categories are rendered at once; no client selection-count maximum is imposed, and discovery may return a server limit.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4379-4391", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3865-3877", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionWorkspace/useConstructionLifecycle.ts:419-425" ] } @@ -1110,9 +1111,9 @@ "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3486-3505", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/RowDefinitionSettingsPanel.tsx:979-1000", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:821-833", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1228-1253", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1953-1994", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:744-756", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1151-1176", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1876-1917", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionWorkspace/useConstructionLifecycle.ts:377-425", "ui/packages/loom-ui/src/api.ts:1652-1667", "ui/packages/loom-ui/src/api.ts:1674-1678" @@ -1248,9 +1249,9 @@ } ], "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2331-2343", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2254-2266", "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3483-3505", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1040-1072", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:963-995", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/CodedPivotEditor.tsx:108-127", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/CodedPivotEditor.tsx:249-277" ], @@ -1288,8 +1289,8 @@ "maximum": null }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4561-4676", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1255-1277" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4047-4162", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1200" ] }, { @@ -1313,8 +1314,8 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4651-4676", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1265-1270" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4137-4162", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1188-1193" ] }, { @@ -1333,8 +1334,8 @@ "maximum": 2 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:835-860", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1255-1277" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:758-783", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1200" ] }, { @@ -1351,7 +1352,7 @@ "maximum": 1 }, "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4632-4635" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4118-4121" ] } ], @@ -1369,8 +1370,8 @@ "basis": "observed-ui", "statement": "Signed source pickers distinguish no listed fields, no populated eligible fields, and eligible populated fields already bound elsewhere. Own bindings count as selectable only when visible and solely bound in the lane; unavailable source-lane reasons stay separate. Existing live field carriers suppress duplicate signed choices only when authoritative field identity resolves unambiguously; stale or ambiguous carriers retain the signed-choice path, and saved bindings are not rewritten. The source-level explanation is derived independently of input rows, including when no inputs have been selected.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4896-4911", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4958-4970", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4382-4397", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:4444-4456", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/existingFieldReuse.ts:22-44", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/sourcePickerExplanations.ts:22-46", "ui/packages/loom-ui/src/features/ExplorerBuilder/components/SourcePickerExplanation.tsx:5-43" @@ -1380,7 +1381,7 @@ "basis": "observed-ui", "statement": "At least one scalar input is required; inputs are unique; NULL and MISSING cannot be key values; output names must be complete and unique.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1260-1270" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1183-1193" ] }, { @@ -1388,15 +1389,15 @@ "statement": "The UI describes one output row per selected input column, with source-name and value outputs; other columns repeat.", "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/components/RowDefinitionSettingsPanel.tsx:1002-1013", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:835-860" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:758-783" ] } ], "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/components/RowDefinitionSettingsPanel.tsx:979-1015", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2354-2364", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:852-860", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1255-1277" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2277-2287", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:775-783", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:1178-1200" ] }, { @@ -1706,7 +1707,7 @@ "evidence": [ "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3507-3521", "ui/packages/loom-ui/src/features/ExplorerBuilder/BuilderWorkspace.tsx:3240-3249", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2371-2381", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2294-2304", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/RelatedExpandEditor.tsx:79-151", "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/RelatedExpandEditor.tsx:278-310", "ui/packages/loom-ui/src/api.ts:1553-1564", @@ -2911,7 +2912,7 @@ "ui_path": "Group and summarize > Summaries", "state": "SUM and MEAN remain in the summary selector but are disabled when there are no numeric columns.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3317-3341" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:3240-3264" ], "basis": "observed-ui" }, @@ -2930,8 +2931,8 @@ "ui_path": "Reshape editor > Reshape table choices", "state": "Turn categories into columns and Turn columns into rows remain rendered disabled with capability reasons when unsupported; unsupported category/policy choices are also disabled in their lists.", "evidence": [ - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2329-2364", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2496-2511" + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2252-2287", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx:2419-2434" ], "basis": "observed-ui" }, diff --git a/docs/ui-contract-inventory/ui-supported-affordances.json b/docs/ui-contract-inventory/ui-supported-affordances.json index 18445449f..39d6542b1 100644 --- a/docs/ui-contract-inventory/ui-supported-affordances.json +++ b/docs/ui-contract-inventory/ui-supported-affordances.json @@ -7,7 +7,7 @@ }, { "path": "docs/ui-contract-inventory/relational.fragment.json", - "sha256": "f0d2e2edac2ab7d7751812f7bec189780f1a47054f711517ffaa9d14c84f9934" + "sha256": "7f65cd0a212d156bb815cdd3e03f7ce43f16c07c2bfe7bed6141a7c04ef75575" }, { "path": "docs/ui-contract-inventory/lifecycle.fragment.json", @@ -18,7 +18,7 @@ "sha256": "f2d2920a2fefa4587d260fff09ae4a0af0c1f4b94d286c1c8a15b088d46421f1" } ], - "expandedSha256": "3d1de6bf8e531684f7c1b5d62c82dd49daebbbad7be2f871e3249bb3882e50c2", + "expandedSha256": "40bb5e9d9549ee49426795cd0f557084d5e34f81fee1a275a5b63964e9daac27", "metadata": { "schemaVersion": 1, "status": "source-backed-provisional; model-and-adapter-coverage-pending", @@ -316,12 +316,16 @@ }, { "path": "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx", - "sha256": "a3f66597ebe7cf9d5f81d04f537e7f3a291b359817f24e670750c2949449f006" + "sha256": "067873e6a16954ff9a07f8ce4ff387c7ce1b5fdff452c956be5fb5c4e0cb9f0b" }, { "path": "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/FilterRowsEditor.tsx", "sha256": "617371f77b152fe96cda55b8753abcf8584bd2ffa0a351061d8513a8cde83562" }, + { + "path": "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/GroupCodedValuePicker.tsx", + "sha256": "1d5ca8307d5d2ea56dadab5596dbd3409a2275ad9ce334adfb6f709549aac8b8" + }, { "path": "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/RelatedEligibilityEditor.tsx", "sha256": "1998bc7643e4ddbe0a11b57a508f66cc774b8c090b06b4b7b8f12a94653e4aa8" diff --git a/verification/builder-interactions/reviewed-sources.json b/verification/builder-interactions/reviewed-sources.json index 219b0c3e4..ac0905e62 100644 --- a/verification/builder-interactions/reviewed-sources.json +++ b/verification/builder-interactions/reviewed-sources.json @@ -19,7 +19,7 @@ "internal/explorer/service.go": "2239c3c7093f390962961d8c45f120c8fbd1afda500a683a1efbc92960666e83", "internal/explorer/lifecycle/construction_proposal.go": "425bd54ec28a7d5162371d268d793e415af4fbc63e498ff6aea18cf5e88f5956", "internal/explorer/authoringv2/construction_change.go": "0ff459d136a393a7b10f502c592f621762b8e4a5a5302c43052390c093cdf816", - "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx": "a3f66597ebe7cf9d5f81d04f537e7f3a291b359817f24e670750c2949449f006", + "ui/packages/loom-ui/src/features/ExplorerBuilder/constructionOperations/ConstructionReshapeEditor.tsx": "067873e6a16954ff9a07f8ce4ff387c7ce1b5fdff452c956be5fb5c4e0cb9f0b", "internal/explorer/lifecycle/population_routes.go": "3f36ba75f855c0c0756471834dc297b00ac4c435b5905bf0baadcbbcdc9cedd7", "internal/explorer/lifecycle/population.go": "4aa83f5ed13e12b931c878ed60c5ff67ef1ef10f4be7c174324206c5b0b80eb1", "internal/explorer/capability/population_route_choice.go": "7244704d39016bfa1d368a1972f583ac22a3d9c4c1efd548325566b8d3dd3d23",