diff --git a/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md b/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md index 2ad04a7..514101f 100644 --- a/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md +++ b/docs/FORMS_RUNTIME_DOMAIN_BOUNDARY.md @@ -62,6 +62,10 @@ Runtime form submissions for validation, drafts, attachments, signatures, status channel, affected and represented party references, authority basis, purpose, notice, responsible function, language, accessibility needs, and field-level source/confidence provenance on the same exact Form revision used digitally +- a responsive operator source editor that classifies each populated field + independently, including its confidence and optional governed declaring-party, + document, or source-system reference; it restores the current revision's + retained source map instead of flattening mixed provenance - append-only assisted read-back outcomes bound to the exact current Form revision, normalized values, attachments, and signatures; draft corrections invalidate earlier confirmation evidence and submission fails closed until a @@ -157,6 +161,9 @@ The immutable outcome identifies the confirming party and operator. Corrections are saved as a new Form revision and require a new read-back. When confirmation cannot be obtained, the operator must choose that outcome and record the exception; policy or downstream review may still stop or escalate the case. +Newly started assisted sessions may be saved as their first ordinary draft; +this is required before read-back whenever the operator entered or corrected +values, attachments, or acknowledgements. ## Approved Intake And Evidence Profiles diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md index c224281..e4b2c36 100644 --- a/docs/INTERFACE_PATTERN_MIGRATION.md +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -30,4 +30,7 @@ instance revisions, validation, receipts, and handoff evidence. Native controls preserve keyboard order, shared dialogs manage focus, changed values use the global unsaved-draft guard, and bounded list/detail regions keep their existing responsive scrolling. English and German catalogues cover -module-owned copy; definition content uses its own published localization. +module-owned copy; definition content uses its own published localization. The +assisted read-back dialog uses one responsive fieldset per populated value so +mixed person, representative, document, system, and derived provenance remains +legible and independently editable at desktop and mobile widths. diff --git a/package.json b/package.json index fbba8aa..fe9d1b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/forms-runtime", - "version": "0.1.19", + "version": "0.1.20", "private": true, "description": "Definition-aware form submissions and service launch for GovOPlaN.", "type": "module", diff --git a/pyproject.toml b/pyproject.toml index e400926..6578fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-forms-runtime" -version = "0.1.19" +version = "0.1.20" description = "Definition-aware form submissions and service launch for GovOPlaN." readme = "README.md" requires-python = ">=3.12" diff --git a/src/govoplan_forms_runtime/backend/manifest.py b/src/govoplan_forms_runtime/backend/manifest.py index 6655033..904dd81 100644 --- a/src/govoplan_forms_runtime/backend/manifest.py +++ b/src/govoplan_forms_runtime/backend/manifest.py @@ -65,7 +65,7 @@ from govoplan_forms_runtime.backend.status_access import FormStatusAccessService MODULE_ID = "forms_runtime" MODULE_NAME = "Forms Runtime" -MODULE_VERSION = "0.1.19" +MODULE_VERSION = "0.1.20" PARTICIPATE_SCOPE = "forms_runtime:submission:participate" ASSIST_SCOPE = "forms_runtime:submission:assist" READ_SCOPE = "forms_runtime:workspace:read" @@ -674,7 +674,7 @@ manifest = ModuleManifest( "stable idempotency key. Rejected effects may be retried. Unknown outcomes must be reconciled before retry to avoid a " "duplicate target. Administrative compensation records verified absence and never deletes a remote target. " "When Records is enabled, only immutable submitted revisions can be resolved for filing; current Forms Runtime access is rechecked and editable drafts fail closed." - " Assisted intake begins with an authenticated, purpose-bound session. The operator records the channel, party and representation references, authority basis, notice, responsible function, language, accessibility support, and per-field sources. Read-back outcomes are append-only and bind the exact current payload. Corrections must first be saved as a new draft revision and confirmed again; an unavailable confirmation requires an explicit exception note." + " Assisted intake begins with an authenticated, purpose-bound session. The operator records the channel, party and representation references, authority basis, notice, responsible function, language, accessibility support, and per-field sources. The operator interface classifies every populated value independently and restores source, confidence, and declaring-party references retained for the current revision. A newly started assisted session can be saved as its first draft before read-back. Read-back outcomes are append-only and bind the exact current payload. Corrections must first be saved as a new draft revision and confirmed again; an unavailable confirmation requires an explicit exception note." " Applicant status policies are attached to one exact published Form revision. Authenticated access is object-bound to the applicant account; assisted intake can bind it only from an explicit account party reference. Email-link mode compares the submitted email without revealing whether it matched, revokes the earlier link on resend, and delegates delivery to Notifications. Permanent-link mode deliberately trades authentication and expiry for possession of a stable high-entropy URL. Disabling a policy immediately suspends all grants issued under it; changing policy fields affects future submissions while existing grants retain their issued access profile." ), layer="configured", @@ -745,7 +745,10 @@ manifest = ModuleManifest( "unveränderliche eingereichte Revisionen für die Ablage aufgelöst werden; der aktuelle Zugriff in Forms Runtime " "wird erneut geprüft und bearbeitbare Entwürfe werden abgewiesen. Assistierte Erfassung beginnt mit einer " "authentifizierten zweckgebundenen Sitzung. Bearbeitende erfassen Kanal, Partei- und Vertretungsverweise, " - "Befugnisgrundlage, Hinweis, zuständige Funktion, Sprache, Barrierefreiheit und Quellen je Feld. " + "Befugnisgrundlage, Hinweis, zuständige Funktion, Sprache, Barrierefreiheit und Quellen je Feld. Die " + "Bearbeitungsoberfläche klassifiziert jeden ausgefüllten Wert unabhängig und stellt für die aktuelle Revision " + "bewahrte Quellen-, Vertrauens- und Erklärendenreferenzen wieder her. Eine neu begonnene assistierte Sitzung " + "kann vor dem Rücklesen als erster Entwurf gespeichert werden. " "Rückleseergebnisse werden nur angefügt und binden die exakten aktuellen Nutzdaten. Korrekturen müssen zuerst " "als neue Entwurfsrevision gespeichert und erneut bestätigt werden; eine nicht mögliche Bestätigung erfordert " "einen ausdrücklichen Ausnahmevermerk. Regeln für Antragstellerstatus sind an eine exakte veröffentlichte " @@ -789,7 +792,6 @@ manifest = ModuleManifest( test_ref="tests/test_forms_runtime.py", known_limits=( "External advanced or qualified signature providers, scheduled expiry cleanup, and target kinds beyond native Case/Workflow handoffs remain adapter depth. Public links intentionally cannot substitute the native authenticated acknowledgement profile.", - "The assisted-intake API retains per-field provenance, while the first operator dialog applies one selected source/confidence profile to all populated values; mixed-source field editing remains UI depth.", "Status policy field changes apply to future submissions; existing grants retain their issued mode and limits, while disabling the policy suspends all of them immediately. Scheduled token cleanup and per-IP throttling remain operations depth.", ), supported_authority_modes=("native_authoritative",), diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py index 855802e..434a6de 100644 --- a/tests/test_interface_documentation_contract.py +++ b/tests/test_interface_documentation_contract.py @@ -38,6 +38,11 @@ class FormsRuntimeInterfaceDocumentationContractTests(unittest.TestCase): ) self.assertIn("submit", reference.metadata["consequence_classes"]) self.assertIn("compensate", reference.metadata["consequence_classes"]) + self.assertIn("every populated value independently", reference.body) + self.assertFalse(any( + "mixed-source field editing remains" in limit + for limit in manifest.architecture.known_limits + )) if __name__ == "__main__": diff --git a/webui/package.json b/webui/package.json index 6cdba47..aafcb55 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/forms-runtime-webui", - "version": "0.1.19", + "version": "0.1.20", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/features/forms/FormInstancePage.tsx b/webui/src/features/forms/FormInstancePage.tsx index cde0aa8..229fc41 100644 --- a/webui/src/features/forms/FormInstancePage.tsx +++ b/webui/src/features/forms/FormInstancePage.tsx @@ -89,8 +89,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex const [assistedOutcome, setAssistedOutcome] = useState("confirmed"); const [assistedMethod, setAssistedMethod] = useState("spoken_readback"); const [assistedConfirmedBy, setAssistedConfirmedBy] = useState(""); - const [assistedSource, setAssistedSource] = useState("person_statement"); - const [assistedConfidence, setAssistedConfidence] = useState("stated"); + const [assistedFieldSources, setAssistedFieldSources] = useState({}); const [assistedNote, setAssistedNote] = useState(""); const load = useCallback(async (signal?: AbortSignal) => { @@ -115,6 +114,11 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex setHandoffs(nextHandoffs.handoffs); setAssistedConfirmations(nextConfirmations.confirmations); setAssistedConfirmedBy((current) => current || assisted?.affected_party_ref || ""); + setAssistedFieldSources(assistedSourcesForRevision( + nextInstance, + assisted, + nextConfirmations.confirmations + )); setValues(nextInstance.values); setAttachmentRefs(nextInstance.attachment_refs); setSignatureRefs(nextInstance.signature_refs); @@ -147,7 +151,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex const canAdmin = hasScope(auth, "forms_runtime:workspace:admin"); const canEditPermission = canParticipate || canWrite; const editable = Boolean(editableLifecycle && canEditPermission); - const canSave = Boolean(instance?.status === "draft" && definition?.allow_drafts && canEditPermission); + const canSave = Boolean(editableLifecycle && definition?.allow_drafts && canEditPermission); const changed = useMemo( () => Boolean(instance && ( JSON.stringify(values) !== JSON.stringify(instance.values) @@ -248,11 +252,9 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex fieldSources: Object.fromEntries( Object.entries(values) .filter(([, value]) => value !== null && value !== undefined && value !== "") - .map(([key]) => [key, { - source: assistedSource, - confidence: assistedConfidence, - declared_by_ref: assistedConfirmedBy.trim() - }]) + .map(([key]) => [key, assistedFieldSources[key] ?? defaultAssistedFieldSource( + assistedConfirmedBy.trim() || assisted.affected_party_ref + )]) ), correctionNote: assistedNote } @@ -277,6 +279,21 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex setSignatureRefs([]); } + function changeAssistedFieldSource( + fieldKey: string, + patch: Partial + ) { + setAssistedFieldSources((current) => ({ + ...current, + [fieldKey]: { + ...(current[fieldKey] ?? defaultAssistedFieldSource( + assistedConfirmedBy.trim() || assisted?.affected_party_ref || "" + )), + ...patch + } + })); + } + async function uploadAttachments(files: File[]) { if (!instance || !definition) return; const available = Math.max(0, definition.max_attachments - attachmentRefs.length); @@ -385,14 +402,14 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex refreshable reloadAction={{ onReload: () => void load(), loading }} className="form-instance-toolbar" - contextActions={<> + contextActions={ {definition && {localized.title}} {instance && } - } + } primaryActions={instance && recordsAvailable && !editableLifecycle ?