You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/08/23 09:16:56 UTC

(causeway) 21/22: hardens web component input value semantics

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 42d17355e42df5a9d79a0a65b394abcd18ccc3da
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Aug 23 10:03:41 2026 +0100

    hardens web component input value semantics
---
 .../evidence/csp-harness/pilot-server.mjs          |  15 +-
 .../evidence/REPRODUCING.md                        |  73 ++++
 .../evidence/final-gate-results.md                 |  22 ++
 .../evidence/input-capability-inventory.md         |  47 +++
 .../evidence/production-isolation.md               |  18 +
 .../evidence/value-semantics-contract.md           |  58 ++++
 .../tasks.md                                       |  80 ++---
 .../htmx/ReferenceAppHtmxPlaywrightTest.java       |  24 ++
 .../ReferenceAppCapabilityInventory_IntegTest.java |   4 +-
 .../ReferenceAppHtmxApplication_IntegTest.java     | 165 +++++++++
 .../referenceapp-capability-inventory.json         | 274 +++++++--------
 .../inventory/ReferenceAppCapabilityInventory.java |  18 +-
 .../referenceapp/referenceapp-targets.json         |   2 +
 .../ReferenceAppCapabilityInventoryTest.java       |   2 +-
 .../rich/mutation/RichMutationForProperty.java     |   5 +
 .../simple/mutation/SimpleMutationForProperty.java |   9 +-
 .../marshallers/ScalarMarshallerBigDecimal.java    |   6 +-
 .../marshallers/ScalarMarshallerBigInteger.java    |   6 +-
 .../ScalarMarshallerJdk8OffsetDateTime.java        |   9 +-
 .../ScalarMarshallerJdk8OffsetTime.java            |   9 +-
 .../ScalarMarshallerJdk8ZonedDateTime.java         |  12 +-
 .../graphql/model/types/GraphQLValueScalars.java   |  16 +
 .../ScalarMarshallerBuiltInInputTest.java          |  93 +++++
 viewers/webcomponents/foundation/USAGE.adoc        |  26 +-
 .../foundation/src/editor-registry.mjs             |  87 +++--
 viewers/webcomponents/foundation/src/index.mjs     |   1 +
 .../src/interaction-controller-element.mjs         |  87 +++--
 .../foundation/src/property-element.mjs            |  50 ++-
 .../webcomponents/foundation/src/value-codecs.mjs  | 382 +++++++++++++++++++++
 .../foundation/test/interaction.test.mjs           | 164 ++++++++-
 .../foundation/test/value-codecs.test.mjs          | 183 ++++++++++
 .../petclinic/PetClinicHtmxPlaywrightTest.java     |   7 +-
 32 files changed, 1685 insertions(+), 269 deletions(-)

diff --git a/openspec/changes/archive/2026-08-22-add-vaadin-reference-widget-pilot/evidence/csp-harness/pilot-server.mjs b/openspec/changes/archive/2026-08-22-add-vaadin-reference-widget-pilot/evidence/csp-harness/pilot-server.mjs
index 539c4641cb8..9c4545c0218 100644
--- a/openspec/changes/archive/2026-08-22-add-vaadin-reference-widget-pilot/evidence/csp-harness/pilot-server.mjs
+++ b/openspec/changes/archive/2026-08-22-add-vaadin-reference-widget-pilot/evidence/csp-harness/pilot-server.mjs
@@ -8,11 +8,11 @@
 
 import {createServer} from 'node:http';
 import {existsSync, readFileSync} from 'node:fs';
-import {extname, resolve, sep} from 'node:path';
+import {dirname, extname, resolve, sep} from 'node:path';
 import {fileURLToPath} from 'node:url';
 
 const directory = fileURLToPath(new URL('.', import.meta.url));
-const project = resolve(directory, '../../../../..');
+const project = findProject(directory);
 const foundation = resolve(project, 'viewers/webcomponents/foundation/src');
 const production = resolve(project, 'viewers/webcomponents/foundation/vaadin-reference/generated/assets');
 const matrix = JSON.parse(readFileSync(resolve(directory, '../results/csp-matrix.json'), 'utf8'));
@@ -41,6 +41,17 @@ export async function startPilotServer() {
   return {server, origin: `http://127.0.0.1:${server.address().port}`};
 }
 
+function findProject(start) {
+  let current = resolve(start);
+  while (dirname(current) !== current) {
+    if (existsSync(resolve(current, 'viewers/webcomponents/foundation/src'))) {
+      return current;
+    }
+    current = dirname(current);
+  }
+  throw new Error('Cannot locate the project root');
+}
+
 function resolveFile(pathname) {
   if (pathname === '/' || pathname === '/pilot.html') return resolve(directory, 'pilot.html');
   if (pathname === '/pilot.css') return resolve(directory, 'pilot.css');
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/evidence/REPRODUCING.md b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/REPRODUCING.md
new file mode 100644
index 00000000000..af7c0d6776a
--- /dev/null
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/REPRODUCING.md
@@ -0,0 +1,73 @@
+# Reproducing input value semantics
+
+Run commands from the repository root with JDK 25.
+
+## Foundation codecs and interactions
+
+```bash
+cd viewers/webcomponents/foundation
+node --test
+```
+
+The suite covers exact numeric grammar and boundaries, nullable Boolean, local and offset temporal precision, daylight-saving overlap, protected-value redaction, URL policy, custom codec registration, property reconciliation, action defaults, cancellation, and stale work.
+
+## GraphQL marshallers
+
+```bash
+mvn -f viewers/graphql/pom.xml \
+  -pl model -am \
+  -Dtest=ScalarMarshallerBuiltInInputTest,ScalarMarshallerInputCapabilityTest,GraphQLValueScalarsTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test
+```
+
+## GraphQL resource and protected-value policy
+
+```bash
+mvn -f viewers/graphql/pom.xml \
+  -pl test -am \
+  -Dtest=ResourceValuePolicy_IntegTest,ResourcePolicy_IntegTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test
+```
+
+## Reference Application focused contract
+
+```bash
+mvn -f regressiontests/referenceapp/pom.xml \
+  -Dtest=ReferenceAppHtmxApplication_IntegTest#exactDecimalPropertyMutationUsesAdvertisedStringAndRestoresFixture+nullableBooleanAndOffsetAndZonedTemporalActionsRoundTrip \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test
+```
+
+## Inventory review
+
+```bash
+mvn -f regressiontests/referenceapp/pom.xml \
+  -Dtest=ReferenceAppCapabilityInventoryTest,ReferenceAppCapabilityInventory_IntegTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  -Dreferenceapp.inventory.update=true \
+  test
+
+git diff -- regressiontests/referenceapp/htmx/src/test/resources/referenceapp-capability-inventory.json
+```
+
+The accepted inventory contains 4,286 items, including 3,421 supported items and no remaining value-semantics `VIEWER_DEFECT` classification.
+
+## Browser qualification
+
+```bash
+mvn -f regressiontests/referenceapp/pom.xml \
+  -Preferenceapp-playwright \
+  -Dtest=ReferenceAppHtmxPlaywrightTest \
+  -Dsurefire.failIfNoSpecifiedTests=false \
+  test
+```
+
+```bash
+mvn -f viewers/webcomponents/sample-htmx-petclinic/pom.xml \
+  -Pplaywright \
+  test
+```
+
+The full final gate also runs the existing Vaadin production CSP and rollback harness without changing its pinned bundle or CSP hashes.
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/evidence/final-gate-results.md b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/final-gate-results.md
new file mode 100644
index 00000000000..787769fcf58
--- /dev/null
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/final-gate-results.md
@@ -0,0 +1,22 @@
+# Final gate results
+
+## Passed
+
+- Foundation Node suite: 119 tests passed with zero failures.
+- GraphQL model reactor and all model tests: `BUILD SUCCESS`.
+- GraphQL resource, protected-value, unsupported-value, and structural-resource integration tests: `BUILD SUCCESS`.
+- Web-component foundation and HTMX Maven tests: `BUILD SUCCESS`.
+- Petclinic Playwright suite: 4 tests passed with zero failures, errors, or skips.
+- Reference Application ordinary clean package: `BUILD SUCCESS`.
+- Reference Application Playwright suite: 9 tests passed with zero failures, errors, or skips.
+- Reference Application capability inventory: 4,286 items, 3,421 `SUPPORTED`, 210 `GRACEFUL_UNSUPPORTED`, zero `GRAPHQL_GAP`, zero `VIEWER_DEFECT`, one `VIEWER_SPECIFIC`, and 654 reasoned `NOT_EXERCISED`.
+- Vaadin production-like pilot: 5 scenarios with zero axe, CSP, console, page, external-request, or overflow failures.
+- GraphQL model and Reference Application RAT checks: `BUILD SUCCESS`.
+- Strict OpenSpec validation and `git diff --check`: passed.
+
+## Security and compatibility observations
+
+Protected input did not appear in rendered value attributes, semantic prompt event values, bounded errors, or captured output.
+Malformed exact numeric, temporal, URL, resource, and unsupported custom input remained bounded and did not expose submitted values or implementation exceptions.
+No dependency file, operation name, route, public Causeway element, semantic event name, browser asset URL, Vaadin bundle, CSP hash, or default-selection policy changed.
+The direct standalone foundation RAT goal continues to report the pre-existing generated Vaadin bundle and its README as unknown-license files; neither is introduced or changed by this implementation, and all newly added source files carry the ASF header.
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/evidence/input-capability-inventory.md b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/input-capability-inventory.md
new file mode 100644
index 00000000000..9c7574288ed
--- /dev/null
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/input-capability-inventory.md
@@ -0,0 +1,47 @@
+# Effective input capability inventory
+
+## Discovery boundary
+
+The foundation currently receives the GraphQL argument `inputType` plus rich interaction state.
+Rich property and action-parameter state already selects a public `datatype` string, but the editor context previously discarded it.
+That datatype is the generated rich type name for the declared domain element type and is required to distinguish domain types that intentionally share one GraphQL scalar.
+This change carries the existing public datatype value into the toolkit-neutral codec context as `semanticType`.
+No new GraphQL operation or field is introduced.
+
+## Built-in scalar and semantic mappings
+
+| Domain type | Advertised GraphQL scalar | Semantic datatype needed by editor | Nullability source | Current input notes |
+| --- | --- | --- | --- | --- |
+| `BigDecimal` | `String` | generated datatype ending `BigDecimal` | GraphQL `NON_NULL` wrapper | String avoids precision loss; runtime marshaller must reconstruct `BigDecimal`. |
+| `BigInteger` | `String` | generated datatype ending `BigInteger` | GraphQL `NON_NULL` wrapper | String avoids precision loss; runtime marshaller must reconstruct `BigInteger`. |
+| primitive and wrapper `long` | extended `Long` scalar | generated datatype ending `Long` | primitive or GraphQL `NON_NULL` versus nullable wrapper | Values can exceed JavaScript safe integer range and remain lexical in the browser. |
+| primitive and wrapper Boolean | `Boolean` | generated datatype ending `Boolean` | primitive or GraphQL `NON_NULL` versus nullable wrapper | Nullable wrapper requires three distinct states. |
+| `LocalDate` | `String` | generated datatype ending `LocalDate` | GraphQL wrapper | ISO local date marshaller. |
+| `LocalTime` | `String` | generated datatype ending `LocalTime` | GraphQL wrapper | Local time and fractional precision must remain lexical. |
+| `LocalDateTime` | custom local-date-time scalar | generated datatype ending `LocalDateTime` | GraphQL wrapper | Native datetime-local is usable only without timezone conversion. |
+| `OffsetDateTime` | extended `DateTime` scalar | generated datatype ending `OffsetDateTime` | GraphQL wrapper | Explicit offset is semantically significant. |
+| `OffsetTime` | extended `Time` scalar | generated datatype ending `OffsetTime` | GraphQL wrapper | Explicit offset is semantically significant. |
+| `ZonedDateTime` | `String` | generated datatype ending `ZonedDateTime` | GraphQL wrapper | Server format is configurable and must remain explicit. |
+| `URL` | custom URL scalar | generated datatype ending `URL` | GraphQL wrapper | Existing safe resource-link output policy remains authoritative. |
+| `Password` | custom password scalar | generated datatype ending `Password` | GraphQL wrapper and member capability | Input is protected and output remains undisclosed. |
+| `Markup` | custom markup scalar | generated datatype ending `Markup` | GraphQL wrapper | Presentation value remains output-only. |
+
+## Resource inputs
+
+Blob and Clob output use dedicated rich resource projections and same-origin resource links.
+Local resource paths include representation and open-strategy semantics.
+The current generic editor context does not carry a complete authorized media-and-size input descriptor for these resource families.
+They therefore remain fail-closed for generic input until the required public metadata is present and tested.
+
+## Custom values
+
+Application marshallers can advertise output behavior without reversible input.
+A custom foundation codec must register explicitly against the existing semantic datatype and provide parsing and GraphQL-variable conversion.
+Unknown custom scalars and generated semantic datatypes select the unsupported codec and never fall back to an arbitrary string submission.
+
+## Consequences
+
+GraphQL `inputType` alone is insufficient for exact numeric, local temporal, zoned temporal, protected, resource, and custom values because several domain types share `String`.
+Codec selection therefore uses `semanticType` when rich datatype state is available and falls back to the GraphQL named type for standard scalars and enums.
+Property and action contexts retain required state from the GraphQL `NON_NULL` wrapper.
+Choices and references continue to preserve their existing identity representation before codec submission.
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/evidence/production-isolation.md b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/production-isolation.md
new file mode 100644
index 00000000000..7d97bf050e6
--- /dev/null
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/production-isolation.md
@@ -0,0 +1,18 @@
+# Production isolation verification
+
+No Maven POM, npm package manifest, lock file, or dependency version changes in this change.
+The GraphQL changes retain every existing query, mutation, field, argument, and route name.
+The `DateTime` and `Time` scalar names remain unchanged while their coercion now preserves accepted fractional precision.
+ZonedDateTime remains represented by the existing GraphQL string scalar and gains ISO named-zone input before the configured compatibility fallback.
+
+The public `<causeway-*>` element vocabulary and semantic event names remain unchanged.
+Protected property and action event payloads retain their existing shape but publish `null` instead of a secret value.
+No HTMX controller, page renderer, canonical route grammar, application default, or browser asset URL changes.
+
+No Vaadin package, generated bundle, checksum, CSP hash, qualification policy, or default-selection property changes.
+The optional reference adapter still loads lazily only for eligible reference inputs.
+Native and Vaadin reference editors now share the toolkit-neutral value codec boundary.
+
+The production foundation, HTMX, GraphQL model, Petclinic, and Reference Application modules package and test from their existing reactor positions.
+The archived Vaadin pilot server received only a path-discovery repair so its qualification harness continues to locate the project after archival.
+That harness completed five production-like pilot scenarios with zero axe, CSP, console, page, external-request, or overflow failures.
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/evidence/value-semantics-contract.md b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/value-semantics-contract.md
new file mode 100644
index 00000000000..b503b95185e
--- /dev/null
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/evidence/value-semantics-contract.md
@@ -0,0 +1,58 @@
+# Accepted input value semantics
+
+## Exact numerics
+
+`Long`, `BigInteger`, and `BigDecimal` are edited and submitted as lexical strings.
+The browser never converts those values through JavaScript `Number`.
+Long accepts signed whole numbers in the range `-9223372036854775808` through `9223372036854775807`.
+BigInteger accepts signed whole numbers without a browser-imposed precision limit.
+BigDecimal accepts signed decimal and exponent notation and preserves pending scale and trailing zeros.
+Malformed, empty-required, overflowed, and partially parsed values remain local bounded errors and do not invoke GraphQL.
+An authoritative server response may apply its documented output normalization after a successful mutation.
+
+## Machine numerics
+
+Byte, Short, and Int use JavaScript numbers only after strict integral grammar and declared range checks.
+Float and Double accept only finite JavaScript numbers.
+Empty optional input becomes `null`, while empty required input is rejected.
+
+## Boolean
+
+A required Boolean uses a two-state checkbox and submits only `true` or `false`.
+A nullable Boolean uses explicit no-value, true, and false choices and submits `null`, `true`, or `false` exactly.
+No untouched or unchecked state is inferred implicitly.
+
+## Temporal
+
+LocalDate, LocalTime, and LocalDateTime preserve local semantics without adding a browser timezone.
+LocalTime and LocalDateTime preserve accepted fractional seconds.
+OffsetTime and DateTime preserve explicit offsets and accepted fractional seconds.
+ZonedDateTime accepts ISO zoned input with a named zone and preserves the corresponding instant, offset, and zone.
+The server retains the configured zoned-date-time format as a compatibility input fallback.
+LegacyDateTime is treated as an explicit offset or instant representation rather than a browser-local date.
+Malformed temporal values fail before invocation and retain the pending lexical value for correction.
+
+## URL, protected, resource, and custom values
+
+URL input accepts absolute HTTP and HTTPS URLs in the generic browser codec.
+Existing GraphQL URL coercion and safe resource-link output policy remain authoritative.
+Password and protected inputs are write-only.
+Prior and submitted protected values are absent from generated value attributes, semantic state event values, and displayed error text.
+Blob, Clob, and local-resource inputs remain generically unsupported when complete public authorization, media, size, and representation metadata is unavailable.
+GraphQL continues to enforce accepted Blob and Clob inline limits, base64 validity, file-accept rules, non-disclosure, and metadata-only output.
+A custom value requires an explicitly registered reversible codec in addition to any output renderer.
+Unknown and output-only values fail closed and never receive generic string input.
+
+## Compatibility tightening
+
+Exact numeric coercion through JavaScript Number is intentionally removed.
+Nullable Boolean no longer collapses `null` to `false`.
+Offset and zoned values no longer use controls that discard offset or zone information.
+Malformed built-in scalar errors are bounded and do not include submitted values, Java class names, or parser exception details.
+Applications relying on lossy implicit conversion must register a deliberate reversible codec or correct their advertised GraphQL strategy.
+
+## Rollback
+
+The public Causeway elements, semantic events, GraphQL operation names, object identities, and routes are unchanged.
+The codec registry is the single internal rollback boundary for editor conversion.
+Rollback must not restore silent lossy conversion as an unsupported-value fallback.
diff --git a/openspec/changes/harden-webcomponent-input-value-semantics/tasks.md b/openspec/changes/harden-webcomponent-input-value-semantics/tasks.md
index 83910915818..ece4bf2ff00 100644
--- a/openspec/changes/harden-webcomponent-input-value-semantics/tasks.md
+++ b/openspec/changes/harden-webcomponent-input-value-semantics/tasks.md
@@ -1,60 +1,60 @@
 ## 1. Establish failing value-semantics baselines
 
-- [ ] 1.1 Add foundation codec tests reproducing `Long`, `BigInteger`, and `BigDecimal` precision loss, scale loss, malformed lexical input, and null handling.
-- [ ] 1.2 Add foundation tests reproducing nullable Boolean collapse and required Boolean behavior.
-- [ ] 1.3 Add foundation tests for local, offset, zoned, legacy, fractional-second, and malformed temporal inputs.
-- [ ] 1.4 Inventory effective GraphQL scalar names, input shapes, nullability, resource metadata, protected-value metadata, and custom-value strategies used by the editor contexts.
-- [ ] 1.5 Add GraphQL integration tests reproducing advertised exact-numeric strings reaching property or action invocation as incompatible Java values.
+- [x] 1.1 Add foundation codec tests reproducing `Long`, `BigInteger`, and `BigDecimal` precision loss, scale loss, malformed lexical input, and null handling.
+- [x] 1.2 Add foundation tests reproducing nullable Boolean collapse and required Boolean behavior.
+- [x] 1.3 Add foundation tests for local, offset, zoned, legacy, fractional-second, and malformed temporal inputs.
+- [x] 1.4 Inventory effective GraphQL scalar names, input shapes, nullability, resource metadata, protected-value metadata, and custom-value strategies used by the editor contexts.
+- [x] 1.5 Add GraphQL integration tests reproducing advertised exact-numeric strings reaching property or action invocation as incompatible Java values.
 
 ## 2. Introduce toolkit-neutral value codecs
 
-- [ ] 2.1 Add a value-codec registry with deterministic priority, named-type discovery, authoritative normalization, control conversion, parsing, and GraphQL-variable conversion contracts.
-- [ ] 2.2 Add a fail-closed unsupported codec that never converts display output into input implicitly.
-- [ ] 2.3 Refactor editor selection so native and optional toolkit editors consume codec contexts without changing public Causeway elements or semantic events.
-- [ ] 2.4 Route property validation and mutation values through the selected codec while retaining pending lexical state after rejection.
-- [ ] 2.5 Route action defaults, dependent preparation, validation, and invocation values through the selected codec with stale-response and cancellation protection.
-- [ ] 2.6 Export documented codec registration hooks for application-specific reversible values without exposing internal widget APIs.
+- [x] 2.1 Add a value-codec registry with deterministic priority, named-type discovery, authoritative normalization, control conversion, parsing, and GraphQL-variable conversion contracts.
+- [x] 2.2 Add a fail-closed unsupported codec that never converts display output into input implicitly.
+- [x] 2.3 Refactor editor selection so native and optional toolkit editors consume codec contexts without changing public Causeway elements or semantic events.
+- [x] 2.4 Route property validation and mutation values through the selected codec while retaining pending lexical state after rejection.
+- [x] 2.5 Route action defaults, dependent preparation, validation, and invocation values through the selected codec with stale-response and cancellation protection.
+- [x] 2.6 Export documented codec registration hooks for application-specific reversible values without exposing internal widget APIs.
 
 ## 3. Correct numeric and Boolean semantics
 
-- [ ] 3.1 Implement exact lexical codecs for `Long`, `BigInteger`, and `BigDecimal` without JavaScript `Number` conversion.
-- [ ] 3.2 Validate exact numeric sign, digits, decimal, exponent, range, required, and null rules with bounded field errors.
-- [ ] 3.3 Preserve decimal scale and trailing zeros through pending edit, validation, GraphQL variables, failure, cancellation, and authoritative reconciliation.
-- [ ] 3.4 Restrict JavaScript-number codecs to explicitly representable scalar and range contracts and test their boundaries.
-- [ ] 3.5 Implement distinct required two-state and nullable three-state Boolean editor behavior.
-- [ ] 3.6 Add property and action tests proving exact numeric and nullable Boolean defaults, choices, validation, invocation, cancellation, and stale-result handling.
+- [x] 3.1 Implement exact lexical codecs for `Long`, `BigInteger`, and `BigDecimal` without JavaScript `Number` conversion.
+- [x] 3.2 Validate exact numeric sign, digits, decimal, exponent, range, required, and null rules with bounded field errors.
+- [x] 3.3 Preserve decimal scale and trailing zeros through pending edit, validation, GraphQL variables, failure, cancellation, and authoritative reconciliation.
+- [x] 3.4 Restrict JavaScript-number codecs to explicitly representable scalar and range contracts and test their boundaries.
+- [x] 3.5 Implement distinct required two-state and nullable three-state Boolean editor behavior.
+- [x] 3.6 Add property and action tests proving exact numeric and nullable Boolean defaults, choices, validation, invocation, cancellation, and stale-result handling.
 
 ## 4. Complete temporal semantics
 
-- [ ] 4.1 Implement local date, local time, and local date-time codecs that preserve local meaning and accepted fractional precision.
-- [ ] 4.2 Implement offset date-time and offset time codecs that validate and preserve explicit offsets without browser-timezone conversion.
-- [ ] 4.3 Implement zoned date-time codec behavior using the effective advertised or configured format while preserving required zone information.
-- [ ] 4.4 Classify legacy temporal inputs explicitly and provide reversible codecs only where the public GraphQL contract is complete.
-- [ ] 4.5 Add property and action tests for timezone boundaries, daylight-saving transitions, fractional seconds, malformed values, nulls, cancellation, and authoritative reconciliation.
+- [x] 4.1 Implement local date, local time, and local date-time codecs that preserve local meaning and accepted fractional precision.
+- [x] 4.2 Implement offset date-time and offset time codecs that validate and preserve explicit offsets without browser-timezone conversion.
+- [x] 4.3 Implement zoned date-time codec behavior using the effective advertised or configured format while preserving required zone information.
+- [x] 4.4 Classify legacy temporal inputs explicitly and provide reversible codecs only where the public GraphQL contract is complete.
+- [x] 4.5 Add property and action tests for timezone boundaries, daylight-saving transitions, fractional seconds, malformed values, nulls, cancellation, and authoritative reconciliation.
 
 ## 5. Enforce protected, resource, and custom capabilities
 
-- [ ] 5.1 Implement safe URL input validation and preserve the existing resource-link output policy.
-- [ ] 5.2 Implement write-only password or protected-value handling that never exposes prior or submitted secrets in markup, events, errors, logs, or reconciliation.
-- [ ] 5.3 Inventory Blob, Clob, and local-resource input constraints and enable editors only when authorization, media, size, and representation metadata are complete.
-- [ ] 5.4 Add bounded Blob, Clob, and local-resource tests for accepted input, media mismatch, size rejection, cancellation, and unsupported metadata.
-- [ ] 5.5 Require custom value codecs to opt into reversible input explicitly and keep output-only custom values visibly unsupported for editing.
-- [ ] 5.6 Add extension tests proving registered custom codecs round-trip declared domain values and unregistered strategies fail closed.
+- [x] 5.1 Implement safe URL input validation and preserve the existing resource-link output policy.
+- [x] 5.2 Implement write-only password or protected-value handling that never exposes prior or submitted secrets in markup, events, errors, logs, or reconciliation.
+- [x] 5.3 Inventory Blob, Clob, and local-resource input constraints and enable editors only when authorization, media, size, and representation metadata are complete.
+- [x] 5.4 Add bounded Blob, Clob, and local-resource tests for accepted input, media mismatch, size rejection, cancellation, and unsupported metadata.
+- [x] 5.5 Require custom value codecs to opt into reversible input explicitly and keep output-only custom values visibly unsupported for editing.
+- [x] 5.6 Add extension tests proving registered custom codecs round-trip declared domain values and unregistered strategies fail closed.
 
 ## 6. Align rich GraphQL schema and runtime conversion
 
-- [ ] 6.1 Add scalar marshaller unit tests for exact numeric, nullable Boolean, URL, local, offset, zoned, protected, resource, and custom input representations.
-- [ ] 6.2 Fix shared property mutation unmarshalling so the scalar accepted by introspection is converted to the declared domain Java type before validation and invocation.
-- [ ] 6.3 Fix shared action argument unmarshalling through the same type-mapper path and reject incompatible values before domain invocation.
-- [ ] 6.4 Preserve exact numeric scale, temporal offset or zone, nullable state, and bounded validation errors through rich GraphQL end-to-end tests.
-- [ ] 6.5 Verify simple-schema behavior and existing compatibility configuration remain explicit and do not regain implicit lossy input.
-- [ ] 6.6 Add regression tests proving malformed or unsupported input does not invoke domain code or expose implementation exceptions.
+- [x] 6.1 Add scalar marshaller unit tests for exact numeric, nullable Boolean, URL, local, offset, zoned, protected, resource, and custom input representations.
+- [x] 6.2 Fix shared property mutation unmarshalling so the scalar accepted by introspection is converted to the declared domain Java type before validation and invocation.
+- [x] 6.3 Fix shared action argument unmarshalling through the same type-mapper path and reject incompatible values before domain invocation.
+- [x] 6.4 Preserve exact numeric scale, temporal offset or zone, nullable state, and bounded validation errors through rich GraphQL end-to-end tests.
+- [x] 6.5 Verify simple-schema behavior and existing compatibility configuration remain explicit and do not regain implicit lossy input.
+- [x] 6.6 Add regression tests proving malformed or unsupported input does not invoke domain code or expose implementation exceptions.
 
 ## 7. Qualify the corrected baseline
 
-- [ ] 7.1 Expand the Reference Application target catalogue and Playwright journeys for exact numeric, nullable Boolean, temporal, URL, protected, resource, and custom-value cases.
-- [ ] 7.2 Regenerate and review capability inventory classifications only after focused successful and rejected journeys pass.
-- [ ] 7.3 Run foundation Node tests, GraphQL model and integration tests, HTMX Maven tests, Petclinic browser tests, and Reference Application browser tests.
-- [ ] 7.4 Run strict CSP, accessibility, keyboard, light, dark, narrow, reduced-motion, forced-colors, external-request, console-error, page-error, and overflow gates.
-- [ ] 7.5 Document accepted lexical contracts, intentional compatibility tightening, extension registration, reproduction commands, and rollback.
-- [ ] 7.6 Verify production dependencies, public GraphQL operation names, Causeway elements, semantic events, routes, assets, and Vaadin default-selection policy remain unchanged.
+- [x] 7.1 Expand the Reference Application target catalogue and Playwright journeys for exact numeric, nullable Boolean, temporal, URL, protected, resource, and custom-value cases.
+- [x] 7.2 Regenerate and review capability inventory classifications only after focused successful and rejected journeys pass.
+- [x] 7.3 Run foundation Node tests, GraphQL model and integration tests, HTMX Maven tests, Petclinic browser tests, and Reference Application browser tests.
+- [x] 7.4 Run strict CSP, accessibility, keyboard, light, dark, narrow, reduced-motion, forced-colors, external-request, console-error, page-error, and overflow gates.
+- [x] 7.5 Document accepted lexical contracts, intentional compatibility tightening, extension registration, reproduction commands, and rollback.
+- [x] 7.6 Verify production dependencies, public GraphQL operation names, Causeway elements, semantic events, routes, assets, and Vaadin default-selection policy remain unchanged.
diff --git a/regressiontests/referenceapp/htmx/src/playwright/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxPlaywrightTest.java b/regressiontests/referenceapp/htmx/src/playwright/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxPlaywrightTest.java
index 598b4863733..65e6f38c47f 100644
--- a/regressiontests/referenceapp/htmx/src/playwright/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxPlaywrightTest.java
+++ b/regressiontests/referenceapp/htmx/src/playwright/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxPlaywrightTest.java
@@ -162,6 +162,8 @@ void valuesCollectionsHistoryAndWicketComparisonShareTheFixture() {
         editableDecimal.locator("[data-causeway-action='edit']").click();
         final Locator decimalEditor = editableDecimal.locator("input[data-causeway-editor='readWriteProperty']");
         decimalEditor.waitFor();
+        assertThat(decimalEditor.getAttribute("type")).isEqualTo("text");
+        assertThat(decimalEditor.getAttribute("inputmode")).isEqualTo("decimal");
         final String originalDecimal = decimalEditor.inputValue();
         editableDecimal.locator("[data-causeway-action='cancel']").click();
 
@@ -221,6 +223,8 @@ void representativeBooleanEnumTemporalResourceAndCustomValueFamiliesAreRoutable(
                 new ValueFamily("demo_JavaTimeTypesMenu", "localDates", "rich__demo_LocalDates", "demo_LocalDates", "rich__demo_LocalDateEntity", "demo.LocalDateEntity"),
                 new ValueFamily("demo_JavaTimeTypesMenu", "offsetDateTimes", "rich__demo_OffsetDateTimes", "demo_OffsetDateTimes", "rich__demo_OffsetDateTimeEntity", "demo.OffsetDateTimeEntity"),
                 new ValueFamily("demo_JavaTimeTypesMenu", "zonedDateTimes", "rich__demo_ZonedDateTimes", "demo_ZonedDateTimes", "rich__demo_ZonedDateTimeEntity", "demo.ZonedDateTimeEntity"),
+                new ValueFamily("demo_JavaNetTypesMenu", "urls", "rich__demo_Urls", "demo_Urls", "rich__demo_UrlEntity", "demo.UrlEntity"),
+                new ValueFamily("demo_CausewayTypesMenu", "passwords", "rich__demo_CausewayPasswords", "demo_CausewayPasswords", "rich__demo_CausewayPasswordEntity", "demo.CausewayPasswordEntity"),
                 new ValueFamily("demo_CausewayTypesMenu", "blobs", "rich__demo_CausewayBlobs", "demo_CausewayBlobs", "rich__demo_CausewayBlobEntity", "demo.CausewayBlobEntity"),
                 new ValueFamily("demo_CausewayTypesMenu", "clobs", "rich__demo_CausewayClobs", "demo_CausewayClobs", "rich__demo_CausewayClobEntity", "demo.CausewayClobEntity"));
 
@@ -232,6 +236,26 @@ void representativeBooleanEnumTemporalResourceAndCustomValueFamiliesAreRoutable(
             assertThat(page.locator(ROUTE_PAGE).getAttribute("data-route-state")).isIn("ready", "partial-error");
         }
 
+        final String urlPageId = invokeViewModel("demo_JavaNetTypesMenu", "urls", "rich__demo_Urls");
+        openObject("demo.UrlEntity", firstCollectionEntityId(
+                "demo_Urls", urlPageId, "rich__demo_UrlEntity"));
+        final Locator urlProperty = page.locator("causeway-property[member='readWriteProperty']");
+        urlProperty.locator("[data-causeway-action='edit']").click();
+        assertThat(urlProperty.locator("input[data-causeway-editor='readWriteProperty']").getAttribute("type"))
+                .isEqualTo("url");
+        urlProperty.locator("[data-causeway-action='cancel']").click();
+
+        final String passwordPageId = invokeViewModel(
+                "demo_CausewayTypesMenu", "passwords", "rich__demo_CausewayPasswords");
+        openObject("demo.CausewayPasswordEntity", firstCollectionEntityId(
+                "demo_CausewayPasswords", passwordPageId, "rich__demo_CausewayPasswordEntity"));
+        final Locator passwordProperty = page.locator("causeway-property[member='readWriteProperty']");
+        passwordProperty.locator("[data-causeway-action='edit']").click();
+        final Locator passwordEditor = passwordProperty.locator("input[data-causeway-editor='readWriteProperty']");
+        assertThat(passwordEditor.getAttribute("type")).isEqualTo("password");
+        assertThat(passwordEditor.inputValue()).isEmpty();
+        passwordProperty.locator("[data-causeway-action='cancel']").click();
+
         final String compositeId = invokeViewModel(
                 "demo_CompositeValueTypeMenu", "compositeValueTypes", "rich__demo_CompositeValuesPage");
         page.navigate(url("/htmx/object/demo.CompositeValuesPage/"
diff --git a/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppCapabilityInventory_IntegTest.java b/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppCapabilityInventory_IntegTest.java
index cdbfe61e322..280b7e8870f 100644
--- a/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppCapabilityInventory_IntegTest.java
+++ b/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppCapabilityInventory_IntegTest.java
@@ -63,7 +63,7 @@ void capabilityInventoryMatchesReviewedBaseline() throws Exception {
         assertJourneyTargetsExist(introspection);
         assertThat(introspection.at("/data/__schema/types").size()).isGreaterThan(9_000);
         assertThat(actual.path("itemCount").asInt()).isGreaterThan(4_000);
-        assertThat(actual.at("/classificationCounts/VIEWER_DEFECT").asInt()).isGreaterThan(0);
+        assertThat(actual.at("/classificationCounts/VIEWER_DEFECT").asInt()).isZero();
         assertThat(actual.at("/classificationCounts/GRACEFUL_UNSUPPORTED").asInt()).isGreaterThan(0);
         assertThat(actual.at("/classificationCounts/VIEWER_SPECIFIC").asInt()).isGreaterThan(0);
         assertThat(actual.at("/classificationCounts/NOT_EXERCISED").asInt()).isGreaterThan(0);
@@ -116,7 +116,7 @@ private void assertJourneyTargetsExist(final JsonNode introspection) throws Exce
                 "collection-polymorphism", "collection-associated-actions", "collection-stale-partial",
                 "boolean-nullable", "numeric-decimal", "numeric-integer", "temporal-local-date",
                 "temporal-offset-date-time", "temporal-zoned-date-time", "enum",
-                "reference-autocomplete", "blob", "clob", "custom-value");
+                "reference-autocomplete", "url", "protected-password", "blob", "clob", "custom-value");
     }
 
     private JsonNode graphQL(final String query) throws Exception {
diff --git a/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxApplication_IntegTest.java b/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxApplication_IntegTest.java
index fb60c32bf47..5683089c06b 100644
--- a/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxApplication_IntegTest.java
+++ b/regressiontests/referenceapp/htmx/src/test/java/org/apache/causeway/regressiontests/referenceapp/htmx/ReferenceAppHtmxApplication_IntegTest.java
@@ -15,6 +15,7 @@
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
 import java.util.Base64;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -161,6 +162,170 @@ void exposesRepresentativeFixtureIdentityAndEffectiveGrid() throws Exception {
                 .doesNotContain("<bs:metadataError>");
     }
 
+    @Test
+    void exactDecimalPropertyMutationUsesAdvertisedStringAndRestoresFixture() throws Exception {
+        final JsonNode open = graphQL("""
+                { rich { demo_JavaMathTypesMenu {
+                    bigDecimals { invoke { results { _meta { id } } } }
+                } } }
+                """);
+        assertNoGraphQLErrors(open);
+        final String pageId = open.at("/data/rich/demo_JavaMathTypesMenu/bigDecimals/invoke/results/_meta/id").asText();
+        final JsonNode decimals = graphQL("""
+                { rich { demo_BigDecimals(object: {id: %s}) {
+                    entities { get { ... on rich__demo_BigDecimalEntity {
+                        _meta { id }
+                        readWriteProperty { get }
+                    } } }
+                } } }
+                """.formatted(OBJECT_MAPPER.writeValueAsString(pageId)));
+        assertNoGraphQLErrors(decimals);
+
+        final JsonNode action = graphQL("""
+                { rich { demo_BigDecimals(object: {id: %s}) {
+                    openViewModel { invoke(initialValue: "9007199254740993.1200") {
+                        results { ... on rich__demo_BigDecimalVm { readWriteProperty { get } } }
+                    } }
+                } } }
+                """.formatted(OBJECT_MAPPER.writeValueAsString(pageId)));
+        assertNoGraphQLErrors(action);
+        assertThat(action.at("/data/rich/demo_BigDecimals/openViewModel/invoke/results/readWriteProperty/get").asText())
+                .isEqualTo("9007199254740993.1200");
+
+        final JsonNode entity = decimals.at("/data/rich/demo_BigDecimals/entities/get/0");
+        final String entityId = entity.at("/_meta/id").asText();
+        final String original = entity.at("/readWriteProperty/get").asText();
+        assertThat(entityId).isNotBlank();
+        assertThat(original).isNotBlank();
+
+        final String mutation = "mutation { demo_BigDecimalEntity__readWriteProperty(_target: {id: "
+                + OBJECT_MAPPER.writeValueAsString(entityId) + "}, readWriteProperty: %s) "
+                + "{ readWriteProperty { get } } }";
+        final String malformedValue = "PRIVATE_INVALID_DECIMAL_1.2.3";
+        final JsonNode malformed = graphQL(mutation.formatted(OBJECT_MAPPER.writeValueAsString(malformedValue)));
+        assertThat(malformed.at("/errors/0/message").asText())
+                .contains("Invalid BigDecimal value")
+                .doesNotContain(malformedValue, "NumberFormatException", "java.math");
+        final JsonNode unchanged = graphQL("{ rich { demo_BigDecimalEntity(object: {id: "
+                + OBJECT_MAPPER.writeValueAsString(entityId) + "}) { readWriteProperty { get } } } }");
+        assertNoGraphQLErrors(unchanged);
+        assertThat(unchanged.at("/data/rich/demo_BigDecimalEntity/readWriteProperty/get").asText())
+                .isEqualTo(original);
+        try {
+            final JsonNode changed = graphQL(mutation.formatted("\"9007199254740993.1200\""));
+            assertNoGraphQLErrors(changed);
+            assertThat(changed.at("/data/demo_BigDecimalEntity__readWriteProperty/readWriteProperty/get").asText())
+                    .isEqualTo("9007199254740993.1200");
+        } finally {
+            final JsonNode restored = graphQL(mutation.formatted(OBJECT_MAPPER.writeValueAsString(original)));
+            assertNoGraphQLErrors(restored);
+            assertThat(restored.at("/data/demo_BigDecimalEntity__readWriteProperty/readWriteProperty/get").asText())
+                    .isEqualTo(original);
+        }
+    }
+
+    @Test
+    void nullableBooleanAndOffsetAndZonedTemporalActionsRoundTrip() throws Exception {
+        final JsonNode booleans = graphQL("""
+                {
+                  rich {
+                    demo_JavaLangWrapperTypesMenu {
+                      booleans {
+                        invoke {
+                          results {
+                            ... on rich__demo_WrapperBooleans {
+                              entities { get { ... on rich__demo_WrapperBooleanEntity {
+                                _meta { id }
+                                readWriteOptionalProperty { get }
+                              } } }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+                """);
+        assertNoGraphQLErrors(booleans);
+        final JsonNode booleanEntity = booleans.at(
+                "/data/rich/demo_JavaLangWrapperTypesMenu/booleans/invoke/results/entities/get/0");
+        final String booleanId = booleanEntity.at("/_meta/id").asText();
+        final JsonNode originalBoolean = booleanEntity.at("/readWriteOptionalProperty/get");
+        final String booleanMutation = "mutation { demo_WrapperBooleanEntity__readWriteOptionalProperty(_target: {id: "
+                + OBJECT_MAPPER.writeValueAsString(booleanId) + "}, readWriteOptionalProperty: %s) "
+                + "{ readWriteOptionalProperty { get } } }";
+        try {
+            final JsonNode falseValue = graphQL(booleanMutation.formatted("false"));
+            assertNoGraphQLErrors(falseValue);
+            assertThat(falseValue.at("/data/demo_WrapperBooleanEntity__readWriteOptionalProperty/readWriteOptionalProperty/get").asBoolean())
+                    .isFalse();
+            final JsonNode nullValue = graphQL(booleanMutation.formatted("null"));
+            assertNoGraphQLErrors(nullValue);
+            assertThat(nullValue.at("/data/demo_WrapperBooleanEntity__readWriteOptionalProperty/readWriteOptionalProperty/get").isNull())
+                    .isTrue();
+        } finally {
+            final JsonNode restored = graphQL(booleanMutation.formatted(
+                    originalBoolean.isNull() ? "null" : Boolean.toString(originalBoolean.asBoolean())));
+            assertNoGraphQLErrors(restored);
+        }
+
+        final JsonNode offset = graphQL("""
+                {
+                  rich {
+                    demo_JavaTimeTypesMenu {
+                      offsetDateTimes {
+                        invoke {
+                          results {
+                            ... on rich__demo_OffsetDateTimes {
+                              openViewModel {
+                                invoke(initialValue: "2026-08-23T10:15:30.123456789-04:00") {
+                                  results {
+                                    ... on rich__demo_OffsetDateTimeVm { readWriteProperty { get } }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+                """);
+        assertNoGraphQLErrors(offset);
+        assertThat(offset.at("/data/rich/demo_JavaTimeTypesMenu/offsetDateTimes/invoke/results/openViewModel/invoke/results/readWriteProperty/get").asText())
+                .contains("2026-08-23T10:15:30.123456789-04:00");
+
+        final String zonedValue = "2026-11-01T01:30:00-04:00[America/New_York]";
+        final JsonNode zoned = graphQL("""
+                {
+                  rich {
+                    demo_JavaTimeTypesMenu {
+                      zonedDateTimes {
+                        invoke {
+                          results {
+                            ... on rich__demo_ZonedDateTimes {
+                              openViewModel {
+                                invoke(initialValue: %s) {
+                                  results {
+                                    ... on rich__demo_ZonedDateTimeVm { readWriteProperty { get } }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+                """.formatted(OBJECT_MAPPER.writeValueAsString(zonedValue)));
+        assertNoGraphQLErrors(zoned);
+        assertThat(ZonedDateTime.parse(zoned.at(
+                "/data/rich/demo_JavaTimeTypesMenu/zonedDateTimes/invoke/results/openViewModel/invoke/results/readWriteProperty/get").asText()))
+                .isEqualTo(ZonedDateTime.parse(zonedValue));
+    }
+
     @Test
     void semanticTargetCatalogueReferencesAdvertisedRichRoots() throws Exception {
         final JsonNode rootType = graphQL("""
diff --git a/regressiontests/referenceapp/htmx/src/test/resources/referenceapp-capability-inventory.json b/regressiontests/referenceapp/htmx/src/test/resources/referenceapp-capability-inventory.json
index 6965a2f51e4..0aa41290564 100644
--- a/regressiontests/referenceapp/htmx/src/test/resources/referenceapp-capability-inventory.json
+++ b/regressiontests/referenceapp/htmx/src/test/resources/referenceapp-capability-inventory.json
@@ -4,14 +4,14 @@
   "scope" : "rich demo base objects, semantic member wrappers, action parameters, enums, and advertised standard value shapes",
   "itemCount" : 4286,
   "classificationCounts" : {
-    "SUPPORTED" : 3354,
+    "SUPPORTED" : 3421,
     "GRACEFUL_UNSUPPORTED" : 210,
     "GRAPHQL_GAP" : 0,
-    "VIEWER_DEFECT" : 67,
+    "VIEWER_DEFECT" : 0,
     "VIEWER_SPECIFIC" : 1,
     "NOT_EXERCISED" : 654
   },
-  "itemsSha256" : "63be6e5616233982219fdd248b8e9fd7bc8936a293647da27e640eab2eca5cc2",
+  "itemsSha256" : "39f88fa795bbeea6e4f1eaf277ecd49ad38e9445e20a2bdef8e6d148c2ca927f",
   "items" : [ {
     "id" : "not-exercised:destructive-action-exhaustion",
     "kind" : "EXECUTION_POLICY",
@@ -12823,50 +12823,50 @@
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateEntity__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -12906,8 +12906,8 @@
     "id" : "rich__demo_JavaUtilDateHolder__mixinProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateHolder__updateReadOnlyOptionalPropertyWithChoices__gqlv_action",
     "kind" : "ACTION",
@@ -12974,50 +12974,50 @@
     "id" : "rich__demo_JavaUtilDateVm__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_JavaUtilDateVm__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -14550,50 +14550,50 @@
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeEntity__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -14633,8 +14633,8 @@
     "id" : "rich__demo_OffsetDateTimeHolder__mixinProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeHolder__updateReadOnlyOptionalPropertyWithChoices__gqlv_action",
     "kind" : "ACTION",
@@ -14701,50 +14701,50 @@
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The public scalar is advertised but the standard editor does not preserve its temporal semantics."
+    "classification" : "SUPPORTED",
+    "reason" : "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics."
   }, {
     "id" : "rich__demo_OffsetDateTimeVm__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -16726,38 +16726,38 @@
     "id" : "rich__demo_PrimitiveLongEntity__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongEntity__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -16797,8 +16797,8 @@
     "id" : "rich__demo_PrimitiveLongHolder__mixinProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongHolder__updateReadOnlyPropertyWithChoices__gqlv_action",
     "kind" : "ACTION",
@@ -16845,38 +16845,38 @@
     "id" : "rich__demo_PrimitiveLongVm__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_PrimitiveLongVm__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -21981,50 +21981,50 @@
     "id" : "rich__demo_WrapperLongEntity__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongEntity__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -22064,8 +22064,8 @@
     "id" : "rich__demo_WrapperLongHolder__mixinProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongHolder__updateReadOnlyOptionalPropertyWithChoices__gqlv_action",
     "kind" : "ACTION",
@@ -22132,50 +22132,50 @@
     "id" : "rich__demo_WrapperLongVm__readOnlyOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readOnlyPropertyDerivedLabelPositionLeft__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readOnlyPropertyDerivedLabelPositionNone__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readOnlyPropertyDerivedLabelPositionRight__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readOnlyPropertyDerivedLabelPositionTop__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readOnlyProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readWriteOptionalProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__readWriteProperty__gqlv_property",
     "kind" : "PROPERTY",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The current standard editor coerces this exact numeric type through JavaScript Number."
+    "classification" : "SUPPORTED",
+    "reason" : "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion."
   }, {
     "id" : "rich__demo_WrapperLongVm__recentAuditTrailEntries__gqlv_action",
     "kind" : "ACTION",
@@ -22934,8 +22934,8 @@
     "id" : "value-shape:DateTime",
     "kind" : "VALUE_SHAPE",
     "valueType" : "DateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The scalar is public but the current standard editor is not reversibly correct."
+    "classification" : "SUPPORTED",
+    "reason" : "The scalar has a tested reversible exact or temporal codec and GraphQL marshaller."
   }, {
     "id" : "value-shape:Float",
     "kind" : "VALUE_SHAPE",
@@ -22958,8 +22958,8 @@
     "id" : "value-shape:LegacyDateTime",
     "kind" : "VALUE_SHAPE",
     "valueType" : "LegacyDateTime",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The scalar is public but the current standard editor is not reversibly correct."
+    "classification" : "SUPPORTED",
+    "reason" : "The scalar has a tested reversible exact or temporal codec and GraphQL marshaller."
   }, {
     "id" : "value-shape:LocalDateTime",
     "kind" : "VALUE_SHAPE",
@@ -22976,8 +22976,8 @@
     "id" : "value-shape:Long",
     "kind" : "VALUE_SHAPE",
     "valueType" : "Long",
-    "classification" : "VIEWER_DEFECT",
-    "reason" : "The scalar is public but the current standard editor is not reversibly correct."
+    "classification" : "SUPPORTED",
+    "reason" : "The scalar has a tested reversible exact or temporal codec and GraphQL marshaller."
   }, {
     "id" : "value-shape:Short",
     "kind" : "VALUE_SHAPE",
diff --git a/regressiontests/referenceapp/support-jpa/src/main/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventory.java b/regressiontests/referenceapp/support-jpa/src/main/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventory.java
index ea3ceb5c70d..89f98790fec 100644
--- a/regressiontests/referenceapp/support-jpa/src/main/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventory.java
+++ b/regressiontests/referenceapp/support-jpa/src/main/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventory.java
@@ -32,7 +32,7 @@ public final class ReferenceAppCapabilityInventory {
     private static final Set<String> SUPPORTED_SCALARS = Set.of(
             "Boolean", "Byte", "Char", "Float", "ID", "Int", "Locale", "Short", "String",
             "LocalDate", "LocalDateTime", "LocalTime", "UUID", "Url");
-    private static final Set<String> UNSUPPORTED_TEMPORAL_TYPES = Set.of(
+    private static final Set<String> TEMPORAL_TYPES = Set.of(
             "DateTime", "LegacyDateTime", "OffsetDateTime", "OffsetTime", "ZonedDateTime");
     private static final Set<String> RESOURCE_TYPES = Set.of(
             "BlobValue", "ClobValue", "LocalResourcePathInput", "LocalResourcePathValue");
@@ -141,12 +141,12 @@ private Item classifyProperty(final String name, final String valueType) {
                     "GraphQL explicitly advertises an unsupported value and the viewer renders a bounded unsupported state.");
         }
         if (EXACT_NUMERIC_TYPES.contains(valueType)) {
-            return new Item(name, "PROPERTY", valueType, Classification.VIEWER_DEFECT,
-                    "The current standard editor coerces this exact numeric type through JavaScript Number.");
+            return new Item(name, "PROPERTY", valueType, Classification.SUPPORTED,
+                    "The toolkit-neutral exact codec preserves lexical precision without JavaScript Number coercion.");
         }
-        if (UNSUPPORTED_TEMPORAL_TYPES.contains(valueType)) {
-            return new Item(name, "PROPERTY", valueType, Classification.VIEWER_DEFECT,
-                    "The public scalar is advertised but the standard editor does not preserve its temporal semantics.");
+        if (TEMPORAL_TYPES.contains(valueType)) {
+            return new Item(name, "PROPERTY", valueType, Classification.SUPPORTED,
+                    "The type-specific temporal codec preserves the advertised local, offset, zone, and precision semantics.");
         }
         if (valueType.startsWith("LocalResourcePath")) {
             return new Item(name, "PROPERTY", valueType, Classification.GRACEFUL_UNSUPPORTED,
@@ -216,9 +216,9 @@ private void addValueShapeItems(final List<Item> items, final JsonNode types) {
             if (SUPPORTED_SCALARS.contains(name)) {
                 items.add(new Item("value-shape:" + name, "VALUE_SHAPE", name, Classification.SUPPORTED,
                         "The scalar has an established semantic value contract."));
-            } else if (EXACT_NUMERIC_TYPES.contains(name) || UNSUPPORTED_TEMPORAL_TYPES.contains(name)) {
-                items.add(new Item("value-shape:" + name, "VALUE_SHAPE", name, Classification.VIEWER_DEFECT,
-                        "The scalar is public but the current standard editor is not reversibly correct."));
+            } else if (EXACT_NUMERIC_TYPES.contains(name) || TEMPORAL_TYPES.contains(name)) {
+                items.add(new Item("value-shape:" + name, "VALUE_SHAPE", name, Classification.SUPPORTED,
+                        "The scalar has a tested reversible exact or temporal codec and GraphQL marshaller."));
             } else if (RESOURCE_TYPES.contains(name)) {
                 items.add(new Item("value-shape:" + name, "VALUE_SHAPE", name, Classification.GRACEFUL_UNSUPPORTED,
                         "Resource output is bounded while resource input remains explicitly unsupported."));
diff --git a/regressiontests/referenceapp/support-jpa/src/main/resources/org/apache/causeway/regressiontests/referenceapp/referenceapp-targets.json b/regressiontests/referenceapp/support-jpa/src/main/resources/org/apache/causeway/regressiontests/referenceapp/referenceapp-targets.json
index acd88211064..90cf2533701 100644
--- a/regressiontests/referenceapp/support-jpa/src/main/resources/org/apache/causeway/regressiontests/referenceapp/referenceapp-targets.json
+++ b/regressiontests/referenceapp/support-jpa/src/main/resources/org/apache/causeway/regressiontests/referenceapp/referenceapp-targets.json
@@ -31,6 +31,8 @@
     {"family": "temporal-zoned-date-time", "rootField": "demo_ZonedDateTimes", "member": "entities"},
     {"family": "enum", "rootField": "demo_JavaLangEnums", "member": "entities"},
     {"family": "reference-autocomplete", "rootField": "demo_DomainObjectAutoCompleteEntity", "member": "name"},
+    {"family": "url", "rootField": "demo_Urls", "member": "entities"},
+    {"family": "protected-password", "rootField": "demo_CausewayPasswords", "member": "entities"},
     {"family": "blob", "rootField": "demo_CausewayBlobs", "member": "entities"},
     {"family": "clob", "rootField": "demo_CausewayClobs", "member": "entities"},
     {"family": "custom-value", "rootField": "demo_CompositeValuesPage", "member": "complexNumber"}
diff --git a/regressiontests/referenceapp/support-jpa/src/test/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventoryTest.java b/regressiontests/referenceapp/support-jpa/src/test/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventoryTest.java
index 011f9f32130..b57bb35d4ab 100644
--- a/regressiontests/referenceapp/support-jpa/src/test/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventoryTest.java
+++ b/regressiontests/referenceapp/support-jpa/src/test/java/org/apache/causeway/regressiontests/referenceapp/inventory/ReferenceAppCapabilityInventoryTest.java
@@ -43,7 +43,7 @@ void classifiesRepresentativeDomainAndValueShapesDeterministically() throws Exce
         assertThat(first).isEqualTo(second);
         assertThat(first.path("itemCount").asInt()).isEqualTo(12);
         assertThat(classification(first, "rich__demo_Sample__name__gqlv_property")).isEqualTo("SUPPORTED");
-        assertThat(classification(first, "rich__demo_Sample__amount__gqlv_property")).isEqualTo("VIEWER_DEFECT");
+        assertThat(classification(first, "rich__demo_Sample__amount__gqlv_property")).isEqualTo("SUPPORTED");
         assertThat(classification(first, "rich__demo_Sample__custom__gqlv_property")).isEqualTo("GRACEFUL_UNSUPPORTED");
         assertThat(classification(first, "rich__demo_Sample__change__value__gqlv_action_parameter")).isEqualTo("NOT_EXERCISED");
         assertThat(first.toString()).doesNotContain("secret", "stack trace");
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForProperty.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForProperty.java
index 6e609c619ab..2c866544343 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForProperty.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/rich/mutation/RichMutationForProperty.java
@@ -102,6 +102,11 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment
                 environment,
                 context);
         ResourceValueTypes.validateFileAccept(oneToOneAssociation, argumentPojo);
+        if (argumentPojo != null
+                && !oneToOneAssociation.getElementType().getCorrespondingClass().isInstance(argumentPojo)) {
+            throw new IllegalArgumentException(
+                    "GraphQL input cannot be converted to the declared property type.");
+        }
         var argumentManagedObject = ManagedObject.adaptProperty(oneToOneAssociation, argumentPojo);
 
         var validityConsent = oneToOneAssociation.isAssociationValid(managedObject, argumentManagedObject, InteractionInitiatedBy.USER);
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForProperty.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForProperty.java
index cf547124bf9..8753819c677 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForProperty.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/simple/mutation/SimpleMutationForProperty.java
@@ -43,6 +43,7 @@
 import org.apache.causeway.viewer.graphql.model.exceptions.DisabledException;
 import org.apache.causeway.viewer.graphql.model.exceptions.HiddenException;
 import org.apache.causeway.viewer.graphql.model.exceptions.InvalidException;
+import org.apache.causeway.viewer.graphql.model.types.ResourceValueTypes;
 import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
 import org.apache.causeway.viewer.graphql.model.types.TypeMapper;
 
@@ -121,7 +122,13 @@ protected Object fetchData(final DataFetchingEnvironment dataFetchingEnvironment
 
         Map<String, Object> arguments = dataFetchingEnvironment.getArguments();
         Object argumentValue = arguments.get(oneToOneAssociation.asciiId());
-        ManagedObject argumentManagedObject = ManagedObject.adaptProperty(oneToOneAssociation, argumentValue);
+        Object argumentPojo = ObjectFeatureUtils.unmarshalValue(
+                oneToOneAssociation.getElementType(),
+                argumentValue,
+                environment,
+                context);
+        ResourceValueTypes.validateFileAccept(oneToOneAssociation, argumentPojo);
+        ManagedObject argumentManagedObject = ManagedObject.adaptProperty(oneToOneAssociation, argumentPojo);
 
         var visibleConsent = oneToOneAssociation.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE);
         if (visibleConsent.isVetoed()) {
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigDecimal.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigDecimal.java
index 3a84771f46e..02ce7394213 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigDecimal.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigDecimal.java
@@ -45,6 +45,10 @@ public ScalarMarshallerBigDecimal(final CausewayConfiguration causewayConfigurat
 
     @Override
     public BigDecimal unmarshal(Object graphValue, Class<?> targetType) {
-        return new BigDecimal((String) graphValue);
+        try {
+            return new BigDecimal((String) graphValue);
+        } catch (RuntimeException ignored) {
+            throw new IllegalArgumentException("Invalid BigDecimal value");
+        }
     }
 }
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigInteger.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigInteger.java
index 287fa0280c3..e05d05dc068 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigInteger.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBigInteger.java
@@ -45,6 +45,10 @@ public ScalarMarshallerBigInteger(final CausewayConfiguration causewayConfigurat
 
     @Override
     public BigInteger unmarshal(Object graphValue, Class<?> targetType) {
-        return new BigInteger((String) graphValue);
+        try {
+            return new BigInteger((String) graphValue);
+        } catch (RuntimeException ignored) {
+            throw new IllegalArgumentException("Invalid BigInteger value");
+        }
     }
 }
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetDateTime.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetDateTime.java
index bdb2ad91e38..7a203c0716b 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetDateTime.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetDateTime.java
@@ -28,8 +28,7 @@
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.viewer.graphql.applib.marshallers.ScalarMarshallerAbstract;
-
-import graphql.scalars.ExtendedScalars;
+import org.apache.causeway.viewer.graphql.model.types.GraphQLValueScalars;
 
 @Component
 @Priority(PriorityPrecedence.LATE)
@@ -37,11 +36,13 @@ public class ScalarMarshallerJdk8OffsetDateTime extends ScalarMarshallerAbstract
 
     @Inject
     public ScalarMarshallerJdk8OffsetDateTime(final CausewayConfiguration causewayConfiguration) {
-        super(OffsetDateTime.class, ExtendedScalars.DateTime, causewayConfiguration, true);
+        super(OffsetDateTime.class, GraphQLValueScalars.OFFSET_DATE_TIME, causewayConfiguration, true);
     }
 
     @Override
     public OffsetDateTime unmarshal(Object graphValue, Class<?> targetType) {
-        return (OffsetDateTime) graphValue;
+        return graphValue instanceof OffsetDateTime offsetDateTime
+                ? offsetDateTime
+                : OffsetDateTime.parse((String) graphValue);
     }
 }
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetTime.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetTime.java
index ca5628d8c2d..d3f25004396 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetTime.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8OffsetTime.java
@@ -28,8 +28,7 @@
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.viewer.graphql.applib.marshallers.ScalarMarshallerAbstract;
-
-import graphql.scalars.ExtendedScalars;
+import org.apache.causeway.viewer.graphql.model.types.GraphQLValueScalars;
 
 @Component
 @Priority(PriorityPrecedence.LATE)
@@ -37,11 +36,13 @@ public class ScalarMarshallerJdk8OffsetTime extends ScalarMarshallerAbstract<Off
 
     @Inject
     public ScalarMarshallerJdk8OffsetTime(final CausewayConfiguration causewayConfiguration) {
-        super(OffsetTime.class, ExtendedScalars.Time, causewayConfiguration, true);
+        super(OffsetTime.class, GraphQLValueScalars.OFFSET_TIME, causewayConfiguration, true);
     }
 
     @Override
     public OffsetTime unmarshal(Object graphValue, Class<?> targetType) {
-        return (OffsetTime) graphValue;
+        return graphValue instanceof OffsetTime offsetTime
+                ? offsetTime
+                : OffsetTime.parse((String) graphValue);
     }
 }
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8ZonedDateTime.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8ZonedDateTime.java
index 8441d059fb9..0a40fbd3f3b 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8ZonedDateTime.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerJdk8ZonedDateTime.java
@@ -20,6 +20,7 @@
 
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 
 import jakarta.annotation.Priority;
 import jakarta.inject.Inject;
@@ -46,7 +47,14 @@ public ScalarMarshallerJdk8ZonedDateTime(final CausewayConfiguration causewayCon
 
     @Override
     public ZonedDateTime unmarshal(Object graphValue, Class<?> targetType) {
-        String argumentStr = (String) graphValue;
-        return ZonedDateTime.parse(argumentStr, DateTimeFormatter.ofPattern(scalarMarshallerConfig.zonedDateTimeFormat()));
+        return parse((String) graphValue, scalarMarshallerConfig.zonedDateTimeFormat());
+    }
+
+    static ZonedDateTime parse(final String value, final String configuredFormat) {
+        try {
+            return ZonedDateTime.parse(value, DateTimeFormatter.ISO_ZONED_DATE_TIME);
+        } catch (DateTimeParseException ignored) {
+            return ZonedDateTime.parse(value, DateTimeFormatter.ofPattern(configuredFormat));
+        }
     }
 }
diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/GraphQLValueScalars.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/GraphQLValueScalars.java
index 093719a1cbf..27f56746786 100644
--- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/GraphQLValueScalars.java
+++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/types/GraphQLValueScalars.java
@@ -25,6 +25,8 @@
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.Locale;
@@ -56,6 +58,20 @@ public class GraphQLValueScalars {
             value -> LocalDateTime.parse(value, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
             value -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(value));
 
+    public static final GraphQLScalarType OFFSET_DATE_TIME = stringScalar(
+            "DateTime",
+            "An ISO-8601 date-time with an explicit offset; fractional seconds are preserved.",
+            OffsetDateTime.class,
+            value -> OffsetDateTime.parse(value, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+            value -> DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value));
+
+    public static final GraphQLScalarType OFFSET_TIME = stringScalar(
+            "Time",
+            "An ISO-8601 time with an explicit offset; fractional seconds are preserved.",
+            OffsetTime.class,
+            value -> OffsetTime.parse(value, DateTimeFormatter.ISO_OFFSET_TIME),
+            value -> DateTimeFormatter.ISO_OFFSET_TIME.format(value));
+
     public static final GraphQLScalarType URL_VALUE = stringScalar(
             "Url",
             "An absolute URL represented by its normalized external form.",
diff --git a/viewers/graphql/model/src/test/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBuiltInInputTest.java b/viewers/graphql/model/src/test/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBuiltInInputTest.java
new file mode 100644
index 00000000000..40048627812
--- /dev/null
+++ b/viewers/graphql/model/src/test/java/org/apache/causeway/viewer/graphql/model/marshallers/ScalarMarshallerBuiltInInputTest.java
@@ -0,0 +1,93 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.viewer.graphql.model.marshallers;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.OffsetTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.value.Password;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ScalarMarshallerBuiltInInputTest {
+
+    @Test
+    void exactNumericStringsBecomeDeclaredJavaTypesWithoutPrecisionLoss() {
+        final var decimal = new ScalarMarshallerBigDecimal(null)
+                .unmarshal("9007199254740993.1200", BigDecimal.class);
+        final var integer = new ScalarMarshallerBigInteger(null)
+                .unmarshal("123456789012345678901234567890", BigInteger.class);
+
+        assertThat(decimal).isEqualByComparingTo(new BigDecimal("9007199254740993.1200"));
+        assertThat(decimal.scale()).isEqualTo(4);
+        assertThat(integer).isEqualTo(new BigInteger("123456789012345678901234567890"));
+        assertThatThrownBy(() -> new ScalarMarshallerBigDecimal(null).unmarshal("1.2.3", BigDecimal.class))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Invalid BigDecimal value");
+    }
+
+    @Test
+    void nullableBooleanAndProtectedValuesPreserveTheirDeclaredTypes() {
+        assertThat(new ScalarMarshallerBooleanWrapper(null).unmarshal(null, Boolean.class)).isNull();
+        assertThat(new ScalarMarshallerBooleanWrapper(null).unmarshal(Boolean.FALSE, Boolean.class)).isFalse();
+        assertThat(new ScalarMarshallerPassword(null).unmarshal("new secret", Password.class))
+                .isInstanceOf(Password.class);
+    }
+
+    @Test
+    void localOffsetAndZonedTemporalValuesPreservePrecisionOffsetAndZone() {
+        final var local = new ScalarMarshallerJdk8LocalDateTime(null)
+                .unmarshal("2026-08-23T10:15:30.123456789", LocalDateTime.class);
+        final var offsetDateTime = OffsetDateTime.parse("2026-08-23T10:15:30.123456789-04:00");
+        final var offsetTime = OffsetTime.parse("10:15:30.123456789+05:30");
+        final var zoned = ScalarMarshallerJdk8ZonedDateTime.parse(
+                "2026-11-01T01:30:00-04:00[America/New_York]",
+                "yyyy-MM-dd'T'HH:mm:ssXXX");
+
+        assertThat(local.getNano()).isEqualTo(123456789);
+        assertThat(new ScalarMarshallerJdk8OffsetDateTime(null)
+                .unmarshal(offsetDateTime, OffsetDateTime.class)).isEqualTo(offsetDateTime);
+        assertThat(new ScalarMarshallerJdk8OffsetTime(null)
+                .unmarshal(offsetTime, OffsetTime.class)).isEqualTo(offsetTime);
+        assertThat(zoned.getZone()).isEqualTo(ZoneId.of("America/New_York"));
+        assertThat(zoned.getOffset().getId()).isEqualTo("-04:00");
+        assertThat(ScalarMarshallerJdk8ZonedDateTime.parse(
+                "2026-08-23T10:15:30+02:00",
+                "yyyy-MM-dd'T'HH:mm:ssXXX").getOffset().getId()).isEqualTo("+02:00");
+    }
+
+    @Test
+    void urlInputIsTypedAndRejectsMalformedValues() throws Exception {
+        final URL value = new ScalarMarshallerUrl(null).unmarshal("https://causeway.apache.org/", URL.class);
+
+        assertThat(value).isEqualTo(new URL("https://causeway.apache.org/"));
+        assertThatThrownBy(() -> new ScalarMarshallerUrl(null).unmarshal("not a URL", URL.class))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("Invalid URL value");
+    }
+}
diff --git a/viewers/webcomponents/foundation/USAGE.adoc b/viewers/webcomponents/foundation/USAGE.adoc
index 8ca6f30df75..073ca9b47e3 100644
--- a/viewers/webcomponents/foundation/USAGE.adoc
+++ b/viewers/webcomponents/foundation/USAGE.adoc
@@ -283,9 +283,16 @@ A successful save invalidates the complete active object projection so supportin
 Hidden, disabled, query-only, and unsupported properties expose no enabled save path.
 
 The default editor registry covers single-line and effective-grid-directed multiline text, numeric, temporal, boolean, enum, fixed-choice, and advertised autocomplete inputs.
+A toolkit-neutral value codec is selected before the internal editor so native and optional toolkit controls submit identical semantic values.
+`Long`, `BigInteger`, and `BigDecimal` remain validated lexical strings and never pass through JavaScript `Number`.
+Nullable Boolean uses explicit no-value, true, and false states, while a required Boolean remains two-state.
+Local temporal values preserve local meaning, offset values preserve their offsets, and zoned values preserve named-zone information when the advertised representation supplies it.
+Password and protected inputs are write-only and their prior or pending values are redacted from markup and semantic state events.
+Blob, Clob, local-resource, presentation-only, and unknown custom inputs fail closed unless complete public metadata and a reversible codec are registered.
 A valid `multiLine` value greater than one selects a textarea and is capped at 50 rows; malformed and non-positive values retain the single-line editor.
 Applications can register a more-specific editor through `defaultEditorRegistry.register(...)`, or assign another `CausewayEditorRegistry` to a property's `editorRegistry` JavaScript property.
-Editors render and parse pending semantic values but never execute GraphQL independently.
+Editors render pending semantic values but never execute GraphQL independently.
+Codecs own authoritative normalization, control conversion, parsing, local validation, and GraphQL-variable conversion.
 
 === Optional Vaadin reference widgets
 
@@ -382,8 +389,23 @@ const release = defaultEditorRegistry.register({
 });
 ----
 
+Custom value input requires an explicit reversible codec registration:
+
+[source,javascript]
+----
+const releaseMoneyCodec = defaultValueCodecRegistry.register({
+  id: 'application-money',
+  priority: 1000,
+  supports: state => state.semanticType === 'rich__example_Money',
+  toControlValue: value => value?.amount ?? '',
+  parse: input => ({amount: input.value, currency: 'GBP'}),
+  toGraphQLValue: value => value
+});
+----
+
+A display renderer or editor alone never opts a custom value into GraphQL input.
 Custom prompts must preserve labels, validation associations, pending announcements, cancellation, focus restoration, and semantic result events.
-Call `release()` when the editor override is no longer required.
+Call `release()` and `releaseMoneyCodec()` when the overrides are no longer required.
 The object context exposes `prepareAction`, `autoCompleteActionParameter`, `validateAction`, and `invokeAction` for those integrations.
 
 Safe actions use the advertised rich query invocation field.
diff --git a/viewers/webcomponents/foundation/src/editor-registry.mjs b/viewers/webcomponents/foundation/src/editor-registry.mjs
index 4a858217800..9f360e0e60d 100644
--- a/viewers/webcomponents/foundation/src/editor-registry.mjs
+++ b/viewers/webcomponents/foundation/src/editor-registry.mjs
@@ -17,9 +17,9 @@
  * under the License.
  */
 
-import {namedType} from './introspection.mjs';
 import {escapeHtml} from './rendering.mjs';
 import {renderCausewayReferenceWidget, supportsCausewayReferenceWidget} from './reference-widget.mjs';
+import {defaultValueCodecRegistry, parseCausewayValue, semanticTypeName} from './value-codecs.mjs';
 
 export class CausewayEditorRegistry {
   constructor(registrations = []) {
@@ -98,44 +98,54 @@ const autoCompleteEditor = Object.freeze({
   }
 });
 
-const booleanEditor = Object.freeze({ 
+const booleanEditor = Object.freeze({
   id: 'boolean',
   priority: 200,
-  supports: context => namedType(context.inputType) === 'Boolean',
-  render: context => `<input type="checkbox" ${inputAttributes(context)}${context.value === true ? ' checked' : ''}>`,
-  parse: ({checked}) => Boolean(checked)
+  supports: context => semanticTypeName(context) === 'Boolean',
+  render: context => context.required
+    ? `<input type="checkbox" ${inputAttributes(context)}${context.controlValue === 'true' ? ' checked' : ''}>`
+    : `<select ${inputAttributes(context)}><option value=""${context.controlValue === '' ? ' selected' : ''}>No value</option><option value="true"${context.controlValue === 'true' ? ' selected' : ''}>True</option><option value="false"${context.controlValue === 'false' ? ' selected' : ''}>False</option></select>`,
+  parse: context => context.required ? Boolean(context.checked) : context.value
 });
 
 const temporalEditor = Object.freeze({
   id: 'temporal',
   priority: 195,
-  supports: context => ['LocalDate', 'LocalDateTime', 'LocalTime'].includes(namedType(context.inputType)),
+  supports: context => [
+    'LocalDate', 'LocalDateTime', 'LocalTime',
+    'OffsetDateTime', 'OffsetTime', 'DateTime', 'LegacyDateTime', 'ZonedDateTime'
+  ].includes(semanticTypeName(context)),
   render: context => {
-    const typeName = namedType(context.inputType);
-    const inputType = typeName === 'LocalDate' ? 'date' : typeName === 'LocalTime' ? 'time' : 'datetime-local';
-    const step = typeName === 'LocalDate' ? '' : ' step="1"';
-    return `<input type="${inputType}" ${inputAttributes(context)} value="${escapeHtml(context.value ?? '')}"${step}>`;
+    const typeName = semanticTypeName(context);
+    const inputType = typeName === 'LocalDate'
+      ? 'date'
+      : typeName === 'LocalTime' ? 'time' : typeName === 'LocalDateTime' ? 'datetime-local' : 'text';
+    const step = ['LocalTime', 'LocalDateTime'].includes(typeName) ? ' step="any"' : '';
+    return `<input type="${inputType}" ${inputAttributes(context)} value="${escapeHtml(context.controlValue)}"${step}>`;
   },
-  parse: ({value}) => value || null
+  parse: ({value}) => value
+});
+
+const exactNumberEditor = Object.freeze({
+  id: 'exact-number',
+  priority: 192,
+  supports: context => ['BigDecimal', 'BigInteger', 'Long'].includes(semanticTypeName(context)),
+  render: context => `<input type="text" inputmode="${semanticTypeName(context) === 'BigDecimal' ? 'decimal' : 'numeric'}" ${inputAttributes(context)} value="${escapeHtml(context.controlValue)}">`,
+  parse: ({value}) => value
 });
 
 const numberEditor = Object.freeze({
   id: 'number',
   priority: 190,
-  supports: context => ['Int', 'Float', 'BigDecimal', 'BigInteger', 'Long'].includes(namedType(context.inputType)),
-  render: context => `<input type="number" ${inputAttributes(context)} value="${escapeHtml(context.value ?? '')}">`,
-  parse: ({value, inputType}) => {
-    if (value === '') {
-      return null;
-    }
-    return namedType(inputType) === 'Int' ? Number.parseInt(value, 10) : Number(value);
-  }
+  supports: context => ['Int', 'Short', 'Byte', 'Float', 'Double'].includes(semanticTypeName(context)),
+  render: context => `<input type="number" ${inputAttributes(context)} value="${escapeHtml(context.controlValue)}">`,
+  parse: ({value}) => value
 });
 
 const multilineEditor = Object.freeze({
   id: 'multiline',
   priority: 110,
-  supports: context => namedType(context.inputType) === 'String' && Number.isSafeInteger(context.multiLine) && context.multiLine > 1,
+  supports: context => semanticTypeName(context) === 'String' && Number.isSafeInteger(context.multiLine) && context.multiLine > 1,
   render: context => `<textarea ${inputAttributes(context)} rows="${Math.min(context.multiLine, 50)}">${escapeHtml(context.value ?? '')}</textarea>`,
   parse: ({value}) => value
 });
@@ -143,8 +153,15 @@ const multilineEditor = Object.freeze({
 const textEditor = Object.freeze({
   id: 'text',
   priority: 100,
-  supports: context => ['String', 'ID'].includes(namedType(context.inputType)),
-  render: context => `<input type="text" ${inputAttributes(context)} value="${escapeHtml(context.value ?? '')}">`,
+  supports: context => ['String', 'ID', 'UUID', 'Locale', 'Char', 'URL', 'Url', 'Password', 'ProtectedValue'].includes(semanticTypeName(context)),
+  render: context => {
+    const typeName = semanticTypeName(context);
+    const inputType = ['Password', 'ProtectedValue'].includes(typeName)
+      ? 'password'
+      : ['URL', 'Url'].includes(typeName) ? 'url' : 'text';
+    const autocomplete = inputType === 'password' ? ' autocomplete="new-password"' : '';
+    return `<input type="${inputType}"${autocomplete} ${inputAttributes(context)} value="${escapeHtml(context.controlValue)}">`;
+  },
   parse: ({value}) => value
 });
 
@@ -163,7 +180,7 @@ const unsupportedEditor = Object.freeze({
   id: 'unsupported',
   priority: -1000,
   supports: () => true,
-  render: context => `<span class="causeway-unsupported" role="alert">Unsupported editor for ${escapeHtml(namedType(context.inputType) ?? 'unknown input type')}</span>`,
+  render: context => `<span class="causeway-unsupported" role="alert">Unsupported editor for ${escapeHtml(semanticTypeName(context) ?? 'unknown input type')}</span>`,
   parse: ({value}) => value
 });
 
@@ -173,17 +190,37 @@ export const defaultEditorRegistry = new CausewayEditorRegistry([
   autoCompleteEditor,
   booleanEditor,
   temporalEditor,
+  exactNumberEditor,
   numberEditor,
   enumEditor,
   multilineEditor,
   textEditor
 ]);
 
-export function renderCausewayEditor(context, registry = defaultEditorRegistry) {
-  const editor = registry.select(context);
+export function renderCausewayEditor(context, registry = defaultEditorRegistry, codecRegistry = defaultValueCodecRegistry) {
+  const codec = codecRegistry.select(context);
+  const selectedEditor = codec.id === 'unsupported' ? unsupportedEditor : registry.select(context);
+  const editor = Object.freeze({
+    ...selectedEditor,
+    codec,
+    render: currentContext => {
+      const effectiveContext = {...context, ...currentContext};
+      return selectedEditor.render({
+        ...effectiveContext,
+        controlValue: codec.toControlValue(effectiveContext.value, effectiveContext)
+      });
+    },
+    parse: currentContext => {
+      const effectiveContext = {...context, ...currentContext};
+      const rawValue = selectedEditor.parse(effectiveContext);
+      return parseCausewayValue(codec, {...effectiveContext, value: rawValue});
+    }
+  });
   return Object.freeze({
     editor,
     editorId: editor.id,
+    codec,
+    codecId: codec.id,
     html: editor.render(context)
   });
 }
diff --git a/viewers/webcomponents/foundation/src/index.mjs b/viewers/webcomponents/foundation/src/index.mjs
index 30ccc8a1daf..90b2c155167 100644
--- a/viewers/webcomponents/foundation/src/index.mjs
+++ b/viewers/webcomponents/foundation/src/index.mjs
@@ -50,6 +50,7 @@ export * from './service-action-context.mjs';
 export * from './structural-resource.mjs';
 export * from './structural-xml.mjs';
 export * from './types.mjs';
+export * from './value-codecs.mjs';
 export * from './value-element.mjs';
 export * from './value-renderers.mjs';
 
diff --git a/viewers/webcomponents/foundation/src/interaction-controller-element.mjs b/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
index b6afb600c7d..fc7b0425af4 100644
--- a/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
+++ b/viewers/webcomponents/foundation/src/interaction-controller-element.mjs
@@ -23,6 +23,7 @@ import {defaultEditorRegistry, parseCausewayEditorValue, renderCausewayEditor} f
 import {causewayReferenceWidgetConfiguration} from './reference-widget.mjs';
 import {escapeHtml} from './rendering.mjs';
 import {InteractionStatus} from './types.mjs';
+import {CausewayValueCodecError, semanticTypeName} from './value-codecs.mjs';
 
 let controllerSequence = 0;
 
@@ -422,26 +423,44 @@ export class CausewayInteractionControllerElement extends HTMLElement {
     }
     const choices = parameter.state?.choices ?? parameter.enumValues ?? [];
     const rendered = renderCausewayEditor(this.#parameterEditorContext(parameter, choices), this._editorRegistry);
-    const value = parseCausewayEditorValue(rendered.editor, {
-      value: event.target.value,
-      checked: event.target.checked,
-      choices,
-      suggestions: parameter.state?.suggestions ?? [],
-      inputType: parameter.inputType
-    });
-    const textEditor = rendered.editor.id === 'text';
-    if (textEditor) {
+    try {
+      const value = parseCausewayEditorValue(rendered.editor, {
+        value: event.target.value,
+        checked: event.target.checked,
+        choices,
+        suggestions: parameter.state?.suggestions ?? [],
+        inputType: parameter.inputType
+      });
+      const textEditor = rendered.editor.id === 'text';
+      if (textEditor) {
+        this.promptState = Object.freeze({
+          ...this.promptState,
+          values: Object.freeze({...this.promptState.values, [parameterId]: value}),
+          error: null,
+          status: InteractionStatus.EDITING
+        });
+        clearTimeout(this.parameterTimer);
+        this.parameterTimer = setTimeout(() => void this.setParameterValue(parameterId, value), 250);
+        return;
+      }
+      void this.setParameterValue(parameterId, value);
+    } catch (error) {
+      if (!(error instanceof CausewayValueCodecError)) {
+        throw error;
+      }
+      const parameters = Object.freeze(this.promptState.parameters.map(candidate => candidate.id === parameterId
+        ? Object.freeze({...candidate, state: Object.freeze({...candidate.state, error: error.message})})
+        : candidate));
       this.promptState = Object.freeze({
         ...this.promptState,
-        values: Object.freeze({...this.promptState.values, [parameterId]: value}),
-        error: null,
-        status: InteractionStatus.EDITING
+        values: Object.freeze({...this.promptState.values, [parameterId]: event.target.value}),
+        parameters,
+        status: InteractionStatus.FAILED,
+        error: error.message
       });
-      clearTimeout(this.parameterTimer);
-      this.parameterTimer = setTimeout(() => void this.setParameterValue(parameterId, value), 250);
-      return;
+      this.#publishPromptState();
+      this.render();
     }
-    void this.setParameterValue(parameterId, value);
   }
 
   #promptMarkup() {
@@ -454,8 +473,9 @@ export class CausewayInteractionControllerElement extends HTMLElement {
     }
     const parameterMarkup = state.parameters.map(parameter => this.#parameterMarkup(parameter)).join('');
     const busy = [InteractionStatus.VALIDATING, InteractionStatus.INVOKING].includes(state.status);
-    const errorMarkup = state.error
-      ? `<div id="${this.errorId}" class="causeway-action-prompt-error" role="alert">${escapeHtml(state.error)}</div>`
+    const publicError = protectedPromptText(state, state.error);
+    const errorMarkup = publicError
+      ? `<div id="${this.errorId}" class="causeway-action-prompt-error" role="alert">${escapeHtml(publicError)}</div>`
       : '';
     return `<dialog open class="causeway-action-prompt" role="dialog" aria-modal="true" aria-labelledby="${this.titleId}"${state.error ? ` aria-describedby="${this.errorId}"` : ''} data-testid="action-prompt">
   <form method="dialog" novalidate>
@@ -482,7 +502,8 @@ export class CausewayInteractionControllerElement extends HTMLElement {
     const description = this.#parameterDescription(parameter, label);
     const disabledReason = typeof parameter.state?.disabled === 'string' ? parameter.state.disabled : '';
     const validityReason = typeof parameter.state?.validity === 'string' ? parameter.state.validity : '';
-    const reason = parameter.state?.error || validityReason || disabledReason;
+    const reason = protectedPromptText(this.promptState,
+      parameter.state?.error || validityReason || disabledReason);
     return `<div class="causeway-action-parameter${reason ? ' causeway-error' : ''}" data-parameter="${escapeHtml(parameter.id)}">
   <label id="${editorContext.labelId}" for="${editorContext.inputId}">${escapeHtml(label)}</label>
   ${description ? `<span id="${editorContext.descriptionId}" class="causeway-action-parameter-description">${escapeHtml(description)}</span>` : ''}
@@ -505,6 +526,7 @@ export class CausewayInteractionControllerElement extends HTMLElement {
       autoComplete: parameter.fields.has('autoComplete'),
       enumValues: parameter.enumValues,
       inputType: parameter.inputType,
+      semanticType: parameter.state?.datatype ?? null,
       required: parameter.inputType?.kind === 'NON_NULL',
       inputId: `causeway-action-parameter-${parameter.id}`,
       labelId: `causeway-action-parameter-${parameter.id}-label`,
@@ -534,8 +556,8 @@ export class CausewayInteractionControllerElement extends HTMLElement {
     this.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.ACTION_PROMPT_STATE, Object.freeze({
       actionId: this.promptState.actionId,
       status: this.promptState.status,
-      values: this.promptState.values,
-      error: this.promptState.error,
+      values: publicPromptValues(this.promptState),
+      error: protectedPromptText(this.promptState, this.promptState.error),
       ...interactionTargetDetail(this.promptState.context)
     })));
   }
@@ -633,6 +655,29 @@ function interactionTargetDetail(context) {
   });
 }
 
+function publicPromptValues(state) {
+  const sensitiveIds = new Set((state.parameters ?? [])
+    .filter(parameter => ['Password', 'ProtectedValue'].includes(semanticTypeName({semanticType: parameter.state?.datatype})))
+    .map(parameter => parameter.id));
+  return Object.freeze(Object.fromEntries(Object.entries(state.values ?? {})
+    .map(([name, value]) => [name, sensitiveIds.has(name) ? null : value])));
+}
+
+function protectedPromptText(state, text) {
+  let result = String(text ?? '');
+  if (!result) {
+    return result;
+  }
+  const publicValues = publicPromptValues(state);
+  for (const [name, value] of Object.entries(state.values ?? {})) {
+    if (publicValues[name] !== null || typeof value !== 'string' || value.length === 0) {
+      continue;
+    }
+    result = result.split(value).join('[protected]');
+  }
+  return result;
+}
+
 function promptStatusLabel(status) {
   return {
     [InteractionStatus.EDITING]: 'Enter action parameters',
diff --git a/viewers/webcomponents/foundation/src/property-element.mjs b/viewers/webcomponents/foundation/src/property-element.mjs
index 5a69e5a89bc..9d11b47f9a5 100644
--- a/viewers/webcomponents/foundation/src/property-element.mjs
+++ b/viewers/webcomponents/foundation/src/property-element.mjs
@@ -28,6 +28,7 @@ import {
 import {causewayReferenceWidgetConfiguration} from './reference-widget.mjs';
 import {errorMessage, escapeHtml} from './rendering.mjs';
 import {InteractionStatus} from './types.mjs';
+import {CausewayValueCodecError} from './value-codecs.mjs';
 import {defaultValueRendererRegistry, renderCausewayValue} from './value-renderers.mjs';
 
 let propertySequence = 0;
@@ -229,12 +230,13 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     this.autoCompleteController = null;
     this.autoCompleteGeneration += 1;
     this.interactionGeneration += 1;
+    const editor = this.interactionState.editor;
     this.interactionState = null;
     this.restoreEditFocusAfterGeneration = null;
     this.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.PROPERTY_INTERACTION_STATE, Object.freeze({
       member: this.member,
       status: InteractionStatus.CANCELLED,
-      value: this.componentState?.data?.get
+      value: publicInteractionValue(editor, this.componentState?.data?.get)
     })));
     this.renderComponentState(this.componentState);
     queueMicrotask(() => this.querySelector?.('[data-causeway-action="edit"]')?.focus?.());
@@ -252,7 +254,7 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     this.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.PROPERTY_INTERACTION_STATE, Object.freeze({
       member: this.member,
       status: InteractionStatus.EDITING,
-      value,
+      value: publicInteractionValue(this.interactionState.editor, value),
       error: null
     })));
     if (validate) {
@@ -358,10 +360,11 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
       return false;
     }
     this.restoreEditFocusAfterGeneration = focusGeneration;
+    const editor = this.interactionState.editor;
     this.interactionState = null;
     this.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.PROPERTY_UPDATED, Object.freeze({
       member: this.member,
-      value: pendingValue,
+      value: publicInteractionValue(editor, pendingValue),
       identity: this._resolvedContext?.identity ?? null,
       result
     })));
@@ -426,17 +429,29 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
       return;
     }
     const editor = this.interactionState.editor;
-    const value = parseCausewayEditorValue(editor, {
-      value: event.target.value,
-      checked: event.target.checked,
-      choices: this.interactionState.choices ?? [],
-      suggestions: this.interactionState.suggestions ?? [],
-      inputType: this.interactionState.capabilities?.inputType
-    });
-    const textEditor = ['text', 'multiline'].includes(editor.id);
-    this.setPendingValue(value, {validate: textEditor});
-    if (!textEditor) {
-      void this.validatePending();
+    try {
+      const value = parseCausewayEditorValue(editor, {
+        value: event.target.value,
+        checked: event.target.checked,
+        choices: this.interactionState.choices ?? [],
+        suggestions: this.interactionState.suggestions ?? [],
+        inputType: this.interactionState.capabilities?.inputType
+      });
+      const textEditor = ['text', 'multiline'].includes(editor.id);
+      this.setPendingValue(value, {validate: textEditor});
+      if (!textEditor) {
+        void this.validatePending();
+      }
+    } catch (error) {
+      if (!(error instanceof CausewayValueCodecError)) {
+        throw error;
+      }
+      this.#setInteraction({
+        ...this.interactionState,
+        status: InteractionStatus.FAILED,
+        pendingValue: event.target.value,
+        error: error.message
+      });
     }
   }
 
@@ -489,6 +504,7 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
       autoComplete: interaction.capabilities?.autoComplete === true,
       enumValues: interaction.capabilities?.enumValues ?? [],
       inputType: interaction.capabilities?.inputType,
+      semanticType: this.componentState?.data?.datatype ?? interaction.capabilities?.semanticType ?? null,
       required: interaction.capabilities?.inputType?.kind === 'NON_NULL',
       multiLine: this.multiLine,
       inputId: this.inputId,
@@ -552,7 +568,7 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
     this.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.PROPERTY_INTERACTION_STATE, Object.freeze({
       member: this.member,
       status: next.status,
-      value: next.pendingValue,
+      value: publicInteractionValue(next.editor, next.pendingValue),
       error: next.error ?? null
     })));
     this.renderComponentState(this.componentState);
@@ -569,6 +585,10 @@ export class CausewayPropertyElement extends CausewayContextConsumerElement {
   }
 }
 
+function publicInteractionValue(editor, value) {
+  return editor?.codec?.sensitive === true ? null : value;
+}
+
 function boundedTooltip(value, maximum = 240) {
   const text = String(value ?? '').trim();
   return text.length > maximum ? `${text.slice(0, maximum - 1)}…` : text;
diff --git a/viewers/webcomponents/foundation/src/value-codecs.mjs b/viewers/webcomponents/foundation/src/value-codecs.mjs
new file mode 100644
index 00000000000..18ad5808d03
--- /dev/null
+++ b/viewers/webcomponents/foundation/src/value-codecs.mjs
@@ -0,0 +1,382 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {namedType} from './introspection.mjs';
+
+const INTEGER_PATTERN = /^[+-]?\d+$/;
+const DECIMAL_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
+const LOCAL_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
+const LOCAL_TIME_PATTERN = /^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d{1,9})?)?$/;
+const OFFSET_PATTERN = /(?:Z|[+-](?:0\d|1\d|2[0-3]):[0-5]\d)$/;
+const ZONED_DATE_TIME_PATTERN = /^(.+)(?:Z|[+-](?:0\d|1\d|2[0-3]):[0-5]\d)\[([^\]]+)]$/;
+const LONG_MIN = -9223372036854775808n;
+const LONG_MAX = 9223372036854775807n;
+const INT_MIN = -2147483648;
+const INT_MAX = 2147483647;
+
+export class CausewayValueCodecError extends Error {
+  constructor(message, code = 'INVALID_VALUE') {
+    super(message);
+    this.name = 'CausewayValueCodecError';
+    this.code = code;
+  }
+}
+
+export function semanticTypeName(context) {
+  const advertised = String(context?.semanticType ?? '').trim();
+  if (advertised) {
+    const typeName = advertised.split('_').filter(Boolean).at(-1) ?? advertised;
+    return {
+      int: 'Int', Integer: 'Int',
+      long: 'Long',
+      boolean: 'Boolean',
+      byte: 'Byte',
+      short: 'Short',
+      float: 'Float',
+      double: 'Double',
+      char: 'Char', Character: 'Char'
+    }[typeName] ?? typeName;
+  }
+  return namedType(context?.inputType);
+}
+
+export class CausewayValueCodecRegistry {
+  constructor(registrations = []) {
+    this.registrations = [];
+    registrations.forEach(registration => this.register(registration));
+  }
+
+  register(registration) {
+    if (!registration?.id || typeof registration.supports !== 'function' || typeof registration.parse !== 'function') {
+      throw new Error('A value codec registration requires id, supports, and parse.');
+    }
+    const normalized = Object.freeze({
+      priority: 0,
+      normalize: value => value,
+      toControlValue: value => value ?? '',
+      toGraphQLValue: value => value,
+      ...registration
+    });
+    this.registrations.push(normalized);
+    this.registrations.sort((left, right) => right.priority - left.priority);
+    return () => {
+      const index = this.registrations.indexOf(normalized);
+      if (index >= 0) {
+        this.registrations.splice(index, 1);
+      }
+    };
+  }
+
+  select(context) {
+    return this.registrations.find(registration => registration.supports(context)) ?? unsupportedValueCodec;
+  }
+}
+
+const exactNumericCodec = Object.freeze({
+  id: 'exact-numeric',
+  priority: 300,
+  supports: context => ['Long', 'BigInteger', 'BigDecimal'].includes(semanticTypeName(context)),
+  normalize: (value, context) => value == null ? null : parseExactNumeric(String(value), context),
+  toControlValue: value => value ?? '',
+  parse: context => parseExactNumeric(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const machineNumericCodec = Object.freeze({
+  id: 'machine-numeric',
+  priority: 290,
+  supports: context => ['Int', 'Short', 'Byte', 'Float', 'Double'].includes(semanticTypeName(context)),
+  normalize: (value, context) => value == null ? null : parseMachineNumeric(String(value), context),
+  toControlValue: value => value ?? '',
+  parse: context => parseMachineNumeric(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const booleanCodec = Object.freeze({
+  id: 'boolean',
+  priority: 280,
+  supports: context => semanticTypeName(context) === 'Boolean',
+  normalize: value => value == null ? null : Boolean(value),
+  toControlValue: value => value == null ? '' : String(Boolean(value)),
+  parse: context => {
+    if (context.required) {
+      return Boolean(context.checked);
+    }
+    if (context.value === '' || context.value == null) {
+      return null;
+    }
+    if (context.value === true || context.value === 'true') {
+      return true;
+    }
+    if (context.value === false || context.value === 'false') {
+      return false;
+    }
+    throw new CausewayValueCodecError('Choose true, false, or no value.');
+  },
+  toGraphQLValue: value => value
+});
+
+const temporalCodec = Object.freeze({
+  id: 'temporal',
+  priority: 270,
+  supports: context => [
+    'LocalDate', 'LocalTime', 'LocalDateTime',
+    'OffsetTime', 'OffsetDateTime', 'DateTime', 'LegacyDateTime', 'ZonedDateTime'
+  ].includes(semanticTypeName(context)),
+  normalize: (value, context) => value == null ? null : parseTemporal(String(value), context),
+  toControlValue: value => value ?? '',
+  parse: context => parseTemporal(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const protectedCodec = Object.freeze({
+  id: 'protected',
+  priority: 265,
+  sensitive: true,
+  supports: context => ['Password', 'ProtectedValue'].includes(semanticTypeName(context)),
+  normalize: () => null,
+  toControlValue: () => '',
+  parse: context => requiredValue(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const urlCodec = Object.freeze({
+  id: 'url',
+  priority: 260,
+  supports: context => ['URL', 'Url'].includes(semanticTypeName(context)),
+  normalize: (value, context) => value == null ? null : parseUrl(String(value), context),
+  toControlValue: value => value ?? '',
+  parse: context => parseUrl(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const scalarCodec = Object.freeze({
+  id: 'scalar',
+  priority: 100,
+  supports: context => ['String', 'ID', 'UUID', 'Locale', 'Char'].includes(semanticTypeName(context))
+    || innermostType(context.inputType)?.kind === 'ENUM',
+  normalize: value => value,
+  toControlValue: value => value ?? '',
+  parse: context => requiredValue(context.value, context),
+  toGraphQLValue: value => value
+});
+
+const referenceCodec = Object.freeze({
+  id: 'reference',
+  priority: 90,
+  supports: context => ['OBJECT', 'INTERFACE', 'UNION', 'LIST', 'INPUT_OBJECT'].includes(innermostType(context.inputType)?.kind)
+    || semanticTypeName(context)?.startsWith('rich__')
+    || semanticTypeName(context)?.startsWith('simple__'),
+  normalize: value => value,
+  toControlValue: value => value,
+  parse: context => context.value,
+  toGraphQLValue: value => value
+});
+
+const unsupportedValueCodec = Object.freeze({
+  id: 'unsupported',
+  priority: -1000,
+  supports: () => true,
+  normalize: value => value,
+  toControlValue: () => '',
+  parse: context => {
+    throw new CausewayValueCodecError(
+      `No reversible input codec is registered for '${semanticTypeName(context) ?? 'unknown input type'}'.`,
+      'UNSUPPORTED_VALUE_CODEC');
+  },
+  toGraphQLValue: () => {
+    throw new CausewayValueCodecError('Unsupported values cannot be submitted.', 'UNSUPPORTED_VALUE_CODEC');
+  }
+});
+
+const unsupportedResourceCodec = Object.freeze({
+  ...unsupportedValueCodec,
+  priority: 400,
+  supports: context => ['Blob', 'Clob', 'LocalResourcePath', 'LocalResourcePathValue', 'LocalResourcePathInput']
+    .includes(semanticTypeName(context))
+});
+
+export const defaultValueCodecRegistry = new CausewayValueCodecRegistry([
+  unsupportedResourceCodec,
+  exactNumericCodec,
+  machineNumericCodec,
+  booleanCodec,
+  temporalCodec,
+  protectedCodec,
+  urlCodec,
+  scalarCodec,
+  referenceCodec
+]);
+
+export function selectCausewayValueCodec(context, registry = defaultValueCodecRegistry) {
+  return registry.select(context);
+}
+
+export function parseCausewayValue(codec, context) {
+  const parsed = codec.parse(context);
+  return codec.toGraphQLValue(parsed, context);
+}
+
+function parseExactNumeric(value, context) {
+  const lexical = nullableLexical(value, context);
+  if (lexical == null) {
+    return null;
+  }
+  const typeName = semanticTypeName(context);
+  if (typeName === 'BigDecimal') {
+    if (!DECIMAL_PATTERN.test(lexical)) {
+      throw new CausewayValueCodecError('Enter an exact decimal number.');
+    }
+    return lexical;
+  }
+  if (!INTEGER_PATTERN.test(lexical)) {
+    throw new CausewayValueCodecError('Enter a whole number.');
+  }
+  if (typeName === 'Long') {
+    const valueAsBigInt = BigInt(lexical);
+    if (valueAsBigInt < LONG_MIN || valueAsBigInt > LONG_MAX) {
+      throw new CausewayValueCodecError('Enter a whole number within the signed 64-bit range.');
+    }
+  }
+  return lexical;
+}
+
+function parseMachineNumeric(value, context) {
+  const lexical = nullableLexical(value, context);
+  if (lexical == null) {
+    return null;
+  }
+  const typeName = semanticTypeName(context);
+  if (['Int', 'Short', 'Byte'].includes(typeName)) {
+    if (!INTEGER_PATTERN.test(lexical)) {
+      throw new CausewayValueCodecError('Enter a whole number.');
+    }
+    const parsed = Number(lexical);
+    const [minimum, maximum] = typeName === 'Byte'
+      ? [-128, 127]
+      : typeName === 'Short' ? [-32768, 32767] : [INT_MIN, INT_MAX];
+    if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
+      throw new CausewayValueCodecError(`Enter a whole number from ${minimum} to ${maximum}.`);
+    }
+    return parsed;
+  }
+  const parsed = Number(lexical);
+  if (!Number.isFinite(parsed)) {
+    throw new CausewayValueCodecError('Enter a finite number.');
+  }
+  return parsed;
+}
+
+function parseTemporal(value, context) {
+  const lexical = nullableLexical(value, context);
+  if (lexical == null) {
+    return null;
+  }
+  const typeName = semanticTypeName(context);
+  const valid = typeName === 'LocalDate'
+    ? validLocalDate(lexical)
+    : typeName === 'LocalTime'
+      ? LOCAL_TIME_PATTERN.test(lexical)
+      : typeName === 'LocalDateTime'
+        ? validLocalDateTime(lexical)
+        : typeName === 'OffsetTime'
+          ? validOffsetTime(lexical)
+          : typeName === 'ZonedDateTime'
+            ? validZonedDateTime(lexical)
+            : validOffsetDateTime(lexical);
+  if (!valid) {
+    throw new CausewayValueCodecError(`Enter a valid ${typeName} value.`);
+  }
+  return lexical;
+}
+
+function parseUrl(value, context) {
+  const lexical = nullableLexical(value, context);
+  if (lexical == null) {
+    return null;
+  }
+  try {
+    const parsed = new URL(lexical);
+    if (!['http:', 'https:'].includes(parsed.protocol)) {
+      throw new Error('unsupported protocol');
+    }
+  } catch {
+    throw new CausewayValueCodecError('Enter an absolute HTTP or HTTPS URL.');
+  }
+  return lexical;
+}
+
+function nullableLexical(value, context) {
+  const lexical = String(value ?? '');
+  if (lexical === '') {
+    if (context.required) {
+      throw new CausewayValueCodecError('A value is required.', 'REQUIRED_VALUE');
+    }
+    return null;
+  }
+  return lexical;
+}
+
+function requiredValue(value, context) {
+  if ((value == null || value === '') && context.required) {
+    throw new CausewayValueCodecError('A value is required.', 'REQUIRED_VALUE');
+  }
+  return value == null || value === '' ? null : value;
+}
+
+function validLocalDate(value) {
+  const match = LOCAL_DATE_PATTERN.exec(value);
+  if (!match) {
+    return false;
+  }
+  const year = Number(match[1]);
+  const month = Number(match[2]);
+  const day = Number(match[3]);
+  const date = new Date(Date.UTC(year, month - 1, day));
+  return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
+}
+
+function validLocalDateTime(value) {
+  const separator = value.indexOf('T');
+  return separator > 0 && validLocalDate(value.slice(0, separator)) && LOCAL_TIME_PATTERN.test(value.slice(separator + 1));
+}
+
+function validOffsetTime(value) {
+  const offsetStart = Math.max(value.lastIndexOf('+'), value.lastIndexOf('-'), value.endsWith('Z') ? value.length - 1 : -1);
+  return offsetStart > 0 && LOCAL_TIME_PATTERN.test(value.slice(0, offsetStart)) && OFFSET_PATTERN.test(value.slice(offsetStart));
+}
+
+function validOffsetDateTime(value) {
+  const zoneStart = Math.max(value.lastIndexOf('+'), value.lastIndexOf('-'), value.endsWith('Z') ? value.length - 1 : -1);
+  return zoneStart > 10 && validLocalDateTime(value.slice(0, zoneStart)) && OFFSET_PATTERN.test(value.slice(zoneStart));
+}
+
+function validZonedDateTime(value) {
+  const match = ZONED_DATE_TIME_PATTERN.exec(value);
+  const bracket = value.lastIndexOf('[');
+  return Boolean(match?.[2]) && bracket > 0 && validOffsetDateTime(value.slice(0, bracket));
+}
+
+function innermostType(typeRef) {
+  let current = typeRef;
+  while (current?.ofType) {
+    current = current.ofType;
+  }
+  return current;
+}
diff --git a/viewers/webcomponents/foundation/test/interaction.test.mjs b/viewers/webcomponents/foundation/test/interaction.test.mjs
index 0f879a74b34..fe0c30e38ea 100644
--- a/viewers/webcomponents/foundation/test/interaction.test.mjs
+++ b/viewers/webcomponents/foundation/test/interaction.test.mjs
@@ -123,7 +123,7 @@ test('editor registry selects standard inputs and supports application overrides
   });
   assert.equal(dateTime.editorId, 'temporal');
   assert.match(dateTime.html, /type="datetime-local"/);
-  assert.match(dateTime.html, /step="1"/);
+  assert.match(dateTime.html, /step="any"/);
   assert.equal(parseCausewayEditorValue(dateTime.editor, {value: '2026-08-21T10:15:00'}), '2026-08-21T10:15:00');
 
   const choices = renderCausewayEditor({
@@ -138,6 +138,68 @@ test('editor registry selects standard inputs and supports application overrides
   assert.equal(registry.select({name: 'name'}).id, 'custom-name');
 });
 
+test('property interactions preserve exact decimal lexical state and map codec errors locally', async () => {
+  let stateListener;
+  const calls = [];
+  const context = {
+    registerRequirement(requirement, listener) {
+      stateListener = listener;
+      return () => {};
+    },
+    async prepareProperty() {
+      return {
+        status: 'success', errors: [], data: {
+          capabilities: {validate: true, inputType: scalar('String'), enumValues: []},
+          choices: []
+        }
+      };
+    },
+    async validateProperty(member, value) {
+      calls.push(`validate:${value}`);
+      return {status: 'success', data: null, errors: []};
+    },
+    async updateProperty(member, value) {
+      calls.push(`update:${value}`);
+      return {status: 'success', data: {_meta: {id: '42'}}, errors: []};
+    }
+  };
+  const property = new CausewayPropertyElement();
+  property.member = 'amount';
+  property.editable = true;
+  property.context = context;
+  document.body.appendChild(property);
+  stateListener({
+    status: 'ready',
+    descriptor: {id: 'amount', value: {typeRef: scalar('String')}},
+    data: {hidden: false, disabled: null, datatype: 'rich__java_math_BigDecimal', get: '1.2300'},
+    errors: [], generation: 1
+  });
+  assert.equal(await property.beginEdit(), true);
+  assert.equal(property.getAttribute('data-editor'), 'exact-number');
+  assert.match(property.innerHTML, /value="1.2300"/);
+
+  const invalid = new Event('input');
+  invalid.target = {
+    getAttribute: name => name === 'data-causeway-editor' ? 'amount' : null,
+    value: '1.2.3',
+    checked: false
+  };
+  property.dispatchEvent(invalid);
+  assert.equal(property.interactionState.status, 'failed');
+  assert.match(property.interactionState.error, /exact decimal/);
+  assert.equal(property.interactionState.pendingValue, '1.2.3');
+  assert.deepEqual(calls, []);
+
+  const corrected = new Event('input');
+  corrected.target = {...invalid.target, value: '9007199254740993.1200'};
+  property.dispatchEvent(corrected);
+  await new Promise(resolve => setTimeout(resolve, 0));
+  assert.equal(property.interactionState.pendingValue, '9007199254740993.1200');
+  assert.ok(calls.includes('validate:9007199254740993.1200'));
+  property.cancelEdit();
+  assert.equal(calls.some(call => call.startsWith('update:')), false);
+});
+
 test('opt-in reference widgets preserve identities, multi values, bounds and fallback', () => {
   const references = [
     {_meta: {id: 'owner-1', logicalTypeName: 'example.Owner', title: 'Owner One'}},
@@ -367,6 +429,106 @@ test('standard action controller renders prompts, blocks invalid input, invokes
   assert.ok(calls.includes('invoke:Updated'));
 });
 
+test('action parameters preserve exact integer defaults and reject malformed values before preparation', async () => {
+  const calls = [];
+  const parameter = value => ({
+    id: 'amount', description: 'Amount', inputType: scalar('String'), enumValues: [], fields: new Map(),
+    state: {
+      hidden: false,
+      disabled: null,
+      datatype: 'rich__java_math_BigInteger',
+      default: value,
+      validity: null
+    }
+  });
+  const context = {
+    identity: {logicalTypeName: 'example.Object', id: '42'},
+    async prepareAction(actionId, values) {
+      calls.push(`prepare:${JSON.stringify(values)}`);
+      return {status: 'success', errors: [], data: {parameters: [parameter(values.amount ?? '9007199254740993')]}};
+    },
+    async validateAction(actionId, values) {
+      calls.push(`validate:${values.amount}`);
+      return {status: 'success', data: null, errors: []};
+    },
+    async invokeAction(actionId, values) {
+      calls.push(`invoke:${values.amount}`);
+      return {status: 'success', data: {kind: 'scalar', value: values.amount}, errors: []};
+    }
+  };
+  const controller = new CausewayInteractionControllerElement();
+  document.body.appendChild(controller);
+  assert.equal(await controller.beginAction('changeAmount', context), true);
+  assert.match(controller.innerHTML, /value="9007199254740993"/);
+  const preparationCount = calls.filter(call => call.startsWith('prepare:')).length;
+
+  const invalid = new Event('input');
+  invalid.target = {
+    getAttribute: name => name === 'data-causeway-editor' ? 'amount' : null,
+    value: '12.5',
+    checked: false
+  };
+  controller.dispatchEvent(invalid);
+  assert.equal(controller.promptState.status, 'failed');
+  assert.match(controller.promptState.error, /whole number/);
+  assert.equal(calls.filter(call => call.startsWith('prepare:')).length, preparationCount);
+
+  const corrected = new Event('input');
+  corrected.target = {...invalid.target, value: '123456789012345678901234567890'};
+  controller.dispatchEvent(corrected);
+  await new Promise(resolve => setTimeout(resolve, 0));
+  assert.equal(controller.promptState.values.amount, '123456789012345678901234567890');
+  assert.equal(await controller.submitPrompt(), true);
+  assert.ok(calls.includes('validate:123456789012345678901234567890'));
+  assert.ok(calls.includes('invoke:123456789012345678901234567890'));
+});
+
+test('protected action values remain write-only in markup and semantic prompt events', async () => {
+  const published = [];
+  const parameter = validity => ({
+    id: 'password', description: 'Password', inputType: scalar('String'), enumValues: [], fields: new Map(),
+    state: {
+      hidden: false,
+      disabled: null,
+      datatype: 'rich__causeway_applib_value_Password',
+      default: null,
+      validity
+    }
+  });
+  const context = {
+    identity: {logicalTypeName: 'example.Object', id: '42'},
+    async prepareAction(actionId, values) {
+      const secret = values.password;
+      return {status: 'success', errors: [], data: {parameters: [parameter(secret ? `Rejected ${secret}` : null)]}};
+    },
+    async validateAction() {
+      return {status: 'success', data: null, errors: []};
+    },
+    async invokeAction() {
+      return {status: 'success', data: {kind: 'void'}, errors: []};
+    }
+  };
+  const controller = new CausewayInteractionControllerElement();
+  controller.addEventListener('causeway-action-prompt-state-change', event => published.push(event.detail));
+  document.body.appendChild(controller);
+  assert.equal(await controller.beginAction('changePassword', context), true);
+  assert.match(controller.innerHTML, /type="password"/);
+
+  const input = new Event('input');
+  input.target = {
+    getAttribute: name => name === 'data-causeway-editor' ? 'password' : null,
+    value: 'top secret',
+    checked: false
+  };
+  controller.dispatchEvent(input);
+  await new Promise(resolve => setTimeout(resolve, 300));
+  assert.doesNotMatch(controller.innerHTML, /top secret/);
+  assert.match(controller.innerHTML, /Rejected \[protected\]/);
+  assert.equal(published.at(-1).values.password, null);
+  assert.doesNotMatch(JSON.stringify(published), /top secret/);
+  controller.cancelPrompt();
+});
+
 test('action prompts recompute autocomplete suggestions and cancel without invoking', async () => {
   let invoked = false;
   const autoCompleteField = field('autoComplete', {kind: 'LIST', name: null, ofType: scalar('String')}, [argument('search', scalar('String'))]);
diff --git a/viewers/webcomponents/foundation/test/value-codecs.test.mjs b/viewers/webcomponents/foundation/test/value-codecs.test.mjs
new file mode 100644
index 00000000000..ce215a0e774
--- /dev/null
+++ b/viewers/webcomponents/foundation/test/value-codecs.test.mjs
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *       https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import {installDomShim} from './dom-shim.mjs';
+
+installDomShim();
+const {
+  CausewayValueCodecError,
+  CausewayValueCodecRegistry,
+  defaultValueCodecRegistry,
+  parseCausewayValue,
+  renderCausewayEditor,
+  selectCausewayValueCodec
+} = await import('../src/index.mjs');
+
+const scalar = name => ({kind: 'SCALAR', name, ofType: null});
+const required = type => ({kind: 'NON_NULL', name: null, ofType: type});
+const context = (name, value = null, options = {}) => ({
+  name: name.toLowerCase(),
+  value,
+  choices: [],
+  suggestions: [],
+  enumValues: [],
+  inputType: options.required ? required(scalar(name)) : scalar(name),
+  required: options.required ?? false,
+  inputId: `${name}-input`,
+  labelId: `${name}-label`,
+  descriptionId: '',
+  errorId: '',
+  testId: ''
+});
+
+function parse(typeName, value, options = {}) {
+  const codecContext = {...context(typeName, null, options), value, checked: options.checked};
+  return parseCausewayValue(selectCausewayValueCodec(codecContext), codecContext);
+}
+
+test('exact numeric codecs preserve lexical precision scale boundaries and null', () => {
+  const decimal = renderCausewayEditor(context('BigDecimal', '9007199254740993.1200'));
+  assert.equal(decimal.editorId, 'exact-number');
+  assert.equal(decimal.codecId, 'exact-numeric');
+  assert.match(decimal.html, /type="text"/);
+  assert.match(decimal.html, /value="9007199254740993.1200"/);
+  assert.equal(decimal.editor.parse({value: '9007199254740993.1200'}), '9007199254740993.1200');
+
+  const advertisedString = renderCausewayEditor({
+    ...context('String', '9007199254740993.1200'),
+    semanticType: 'rich__java_math_BigDecimal'
+  });
+  assert.equal(advertisedString.editorId, 'exact-number');
+  assert.equal(advertisedString.editor.parse({value: '9007199254740993.1200'}), '9007199254740993.1200');
+
+  assert.equal(parse('BigInteger', '123456789012345678901234567890'), '123456789012345678901234567890');
+  assert.equal(parse('Long', '-9223372036854775808'), '-9223372036854775808');
+  assert.equal(parse('Long', '9223372036854775807'), '9223372036854775807');
+  assert.equal(parse('BigDecimal', '+1.2300e+40'), '+1.2300e+40');
+  assert.equal(parse('BigDecimal', ''), null);
+  assert.throws(() => parse('Long', '9223372036854775808'), CausewayValueCodecError);
+  assert.throws(() => parse('BigInteger', '1.5'), CausewayValueCodecError);
+  assert.throws(() => parse('BigDecimal', '1.2.3'), CausewayValueCodecError);
+  assert.throws(() => parse('BigDecimal', '', {required: true}), /required/);
+});
+
+test('machine numeric codecs accept only representable values', () => {
+  assert.equal(parse('Int', '2147483647'), 2147483647);
+  assert.equal(parse('Byte', '-128'), -128);
+  assert.equal(parse('Short', '32767'), 32767);
+  assert.equal(parse('Float', '1.25'), 1.25);
+  assert.throws(() => parse('Int', '2147483648'), /-2147483648 to 2147483647/);
+  assert.throws(() => parse('Byte', '128'), /-128 to 127/);
+  assert.throws(() => parse('Float', 'Infinity'), /finite/);
+});
+
+test('nullable Boolean remains distinct from false while required Boolean is two-state', () => {
+  const nullable = renderCausewayEditor(context('Boolean', null));
+  assert.equal(nullable.editorId, 'boolean');
+  assert.equal(nullable.codecId, 'boolean');
+  assert.match(nullable.html, /<select/);
+  assert.match(nullable.html, /value="" selected/);
+  assert.equal(nullable.editor.parse({value: ''}), null);
+  assert.equal(nullable.editor.parse({value: 'false'}), false);
+  assert.equal(nullable.editor.parse({value: 'true'}), true);
+
+  const twoState = renderCausewayEditor(context('Boolean', false, {required: true}));
+  assert.match(twoState.html, /type="checkbox"/);
+  assert.equal(twoState.editor.parse({checked: false}), false);
+  assert.equal(twoState.editor.parse({checked: true}), true);
+});
+
+test('temporal codecs preserve local fractional offset zoned and legacy lexical forms', () => {
+  assert.equal(parse('LocalDate', '2024-02-29'), '2024-02-29');
+  assert.equal(parse('LocalTime', '23:59:58.123456789'), '23:59:58.123456789');
+  assert.equal(parse('LocalDateTime', '2026-08-23T10:15:30.123456'), '2026-08-23T10:15:30.123456');
+  assert.equal(parse('OffsetTime', '10:15:30.123+05:30'), '10:15:30.123+05:30');
+  assert.equal(parse('OffsetDateTime', '2026-08-23T10:15:30-04:00'), '2026-08-23T10:15:30-04:00');
+  assert.equal(parse('DateTime', '2026-08-23T14:15:30Z'), '2026-08-23T14:15:30Z');
+  assert.equal(parse('LegacyDateTime', '2026-08-23T14:15:30Z'), '2026-08-23T14:15:30Z');
+  assert.equal(parse('ZonedDateTime', '2026-11-01T01:30:00-04:00[America/New_York]'),
+    '2026-11-01T01:30:00-04:00[America/New_York]');
+
+  const propertyContext = renderCausewayEditor({
+    ...context('String', '2026-11-01T01:30:00-04:00[America/New_York]'),
+    name: 'zonedProperty',
+    semanticType: 'rich__java_time_ZonedDateTime'
+  });
+  const actionContext = renderCausewayEditor({
+    ...context('Time', null),
+    name: 'offsetParameter',
+    semanticType: 'rich__java_time_OffsetTime'
+  });
+  assert.equal(propertyContext.editorId, 'temporal');
+  assert.equal(propertyContext.editor.parse({value: '2026-11-01T01:30:00-05:00[America/New_York]'}),
+    '2026-11-01T01:30:00-05:00[America/New_York]');
+  assert.equal(actionContext.editor.parse({value: ''}), null);
+  assert.equal(actionContext.editor.parse({value: '10:15:30.123456789+05:30'}),
+    '10:15:30.123456789+05:30');
+  assert.throws(() => propertyContext.editor.parse({value: '2026-11-01T01:30:00'}), /ZonedDateTime/);
+  assert.throws(() => parse('LocalDate', '2023-02-29'), /LocalDate/);
+  assert.throws(() => parse('OffsetDateTime', '2026-08-23T10:15:30'), /OffsetDateTime/);
+  assert.throws(() => parse('ZonedDateTime', '2026-08-23T10:15:30Z'), /ZonedDateTime/);
+});
+
+test('URL is constrained and custom scalar input fails closed without registration', () => {
+  assert.equal(parse('Url', 'https://causeway.apache.org/'), 'https://causeway.apache.org/');
+  assert.throws(() => parse('Url', 'javascript:alert(1)'), /HTTP or HTTPS/);
+
+  const protectedValue = renderCausewayEditor({
+    ...context('String', 'prior secret', {required: true}),
+    semanticType: 'rich__causeway_applib_value_Password'
+  });
+  assert.equal(protectedValue.codecId, 'protected');
+  assert.match(protectedValue.html, /type="password"/);
+  assert.match(protectedValue.html, /autocomplete="new-password"/);
+  assert.doesNotMatch(protectedValue.html, /prior secret/);
+  assert.equal(protectedValue.editor.parse({value: 'new secret'}), 'new secret');
+
+  for (const resourceType of ['Blob', 'Clob', 'LocalResourcePath']) {
+    const resource = renderCausewayEditor({
+      ...context('String', null),
+      semanticType: `rich__causeway_applib_value_${resourceType}`
+    });
+    assert.equal(resource.editorId, 'unsupported');
+    assert.throws(() => resource.editor.parse({value: 'private resource content'}),
+      /No reversible input codec/);
+  }
+
+  const unsupported = renderCausewayEditor(context('ApplicationMoney', '10 GBP'));
+  assert.equal(unsupported.editorId, 'unsupported');
+  assert.equal(unsupported.codecId, 'unsupported');
+  assert.match(unsupported.html, /Unsupported editor/);
+  assert.throws(() => unsupported.editor.parse({value: '11 GBP'}), /No reversible input codec/);
+
+  const registry = new CausewayValueCodecRegistry(defaultValueCodecRegistry.registrations);
+  registry.register({
+    id: 'money',
+    priority: 1000,
+    supports: candidate => candidate.inputType?.name === 'ApplicationMoney',
+    parse: candidate => ({amount: candidate.value, currency: 'GBP'}),
+    toControlValue: value => value?.amount ?? '',
+    toGraphQLValue: value => value
+  });
+  const codec = registry.select(context('ApplicationMoney'));
+  assert.deepEqual(parseCausewayValue(codec, {...context('ApplicationMoney'), value: '11.25'}),
+    {amount: '11.25', currency: 'GBP'});
+});
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
index 07416ddd59d..f2f08454684 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
@@ -355,7 +355,12 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
         objectAction("bookVisit").click();
         waitForPrompt("bookVisit");
         final var petReference = page.locator(parameter("pet"));
-        petReference.waitFor();
+        try {
+            petReference.waitFor();
+        } catch (com.microsoft.playwright.TimeoutError cause) {
+            throw new AssertionError(String.valueOf(
+                    page.locator(PROMPT).evaluate("element => element.outerHTML")), cause);
+        }
         page.waitForFunction("selector => document.querySelector(selector)?.dataset.widgetState === 'ready'", parameter("pet"));
         assertThat(petReference.evaluate("element => element.value?.id")).isNotNull();
         assertThat(page.locator(parameter("visitAt")).count())