Skip to content

Live commit-dialog validation: advisory workflow checks as you type - #758

Merged
fiskus merged 12 commits into
mainfrom
commit-dialog-live-validation
Jul 10, 2026
Merged

Live commit-dialog validation: advisory workflow checks as you type#758
fiskus merged 12 commits into
mainfrom
commit-dialog-live-validation

Conversation

@fiskus

@fiskus fiskus commented Jul 10, 2026

Copy link
Copy Markdown
Member

Summary

The QuiltSync commit dialog now validates input against the selected workflow's rules live, as the user types (debounced 400ms), surfacing violations before the commit attempt instead of as a commit-time refusal:

  • The commit message and user-metadata JSON are checked per keystroke against the workflow's message-required rule and metadata schema; violations render inline under the offending field.
  • The package name is checked against handle_pattern when rules load (the field is read-only in the dialog).
  • Advisory only: the commit buttons stay enabled and the commit-time gate remains the sole authority. "None", ungoverned buckets, and malformed configs show no validation UI.
  • Message/metadata violations appear only after the user has edited that field — a pristine dialog never opens covered in red. Name violations show immediately (their field can't be edited, so eager display is their only path).

Design

  • Backend: load_workflow_rules fetches and compiles the selected workflow's rules once per (namespace, workflow) into Tauri-managed state, refreshed per dialog open and single-flighted under concurrency; validate_commit_candidate is a pure cache read — zero network I/O on the keystroke path.
  • quilt-rs: new public workflow::validate_candidate_fields + InstalledPackage::workflow_rules; the commit gate's validate_package was refactored to share the same helpers, behavior-preserving (entries validation deliberately stays with the gate).
  • Metadata parity: an empty editor validates the previous revision's metadata — exactly what the gate's UserMeta::Keep semantics will validate at commit.
  • UI: typed camelCase wire structs mirrored across the Tauri boundary; debounced validation with the same stale-response self-keying as the workflow selector, so late responses can never paint for outdated input or a different workflow.

Testing

TDD throughout: gate-parity unit tests in quilt-rs (message/metadata/handle-pattern, entries skip); backend cache tests (fetch-once, per-mount refresh, concurrent single-flight via tokio::join!, ungoverned no-op, parse-error violation); byte-identical wire-form tests both sides; UI native tests for per-field violation routing, pristine/dirty display gating, debounce key logic, and effective-metadata selection. Full gate green (fmt, clippy native + wasm with denied warnings, 595+ tests, rumdl).

Versions

quilt-rs 0.33.0-alpha8, quilt-sync 0.18.3-alpha8.

Greptile Summary

This PR adds live, advisory workflow validation to the QuiltSync commit dialog: as the user types (debounced 400ms), the message, user-metadata, and package name are checked against the selected workflow's rules, surfacing inline violations per-field before the commit attempt. The commit-time gate is unchanged and remains authoritative; the new checks are purely informational and never block the commit buttons.

  • Backend: WorkflowRulesCache (app-lifetime Tauri state) holds compiled rules keyed by (namespace, workflow_id) in Arc<OnceCell<…>> slots that single-flight concurrent fetches; load_workflow_rules populates the cache (with a per-session namespace refresh) and validate_commit_candidate is a pure cache read with no I/O on the keystroke path. Parse errors on user metadata coexist with message/handle violations rather than early-returning, matching the commit gate's own field-ordering contract.
  • quilt-rs: validate_package is refactored into shared check_field_rules / check_entries_rule / finish helpers; validate_candidate_fields reuses the field helpers while deliberately omitting the entries schema (entries validation stays at commit time). InstalledPackage::workflow_rules exposes the fetch path for the cache layer.
  • UI (commit.rs): A debounced LocalResource drives the validation round-trip; live_violations self-keys against the current input to discard stale responses. Message and metadata violations are gated by per-field dirtiness so a pristine form never opens with pre-painted errors; name violations show immediately. Suspense is replaced by Transition so the JSON editor is never unmounted during refetches, preventing the focus-loss bug reported after the initial implementation.

Confidence Score: 5/5

The change is safe to merge: it is purely additive and advisory — no existing commit gate logic is altered, only reorganised into shared helpers that the new and old paths both call. The previously flagged issues (parse-error early-return swallowing co-present violations, naming ambiguity, JSON editor focus loss on refetch) are all resolved with accompanying tests.

The field-level refactor of validate_package is behavior-preserving: the extracted helpers are called in the same order with the same logic, and a separate test confirms the entries-schema path is correctly excluded from the candidate path. The cache design (Mutex released before await, Arc+OnceCell single-flight, namespace-scoped refresh on dialog open) is sound under Tokio's concurrency model. The UI stale-response self-keying, dirtiness gating, and Transition boundary all address concrete correctness and UX problems identified during review. Test coverage is thorough across cache, validation, wire-form, and UI view-model layers.

No files require special attention.

Important Files Changed

Filename Overview
quilt-rs/src/workflow/validate.rs Refactors validate_package into shared helpers (check_field_rules, check_entries_rule, finish) and adds the new public validate_candidate_fields that reuses the field-level helpers while deliberately skipping entries; well-tested with gate-parity and entries-skip unit tests.
quilt-sync/src-tauri/src/commands/commit_data.rs Adds WorkflowRulesCache (Arc+OnceCell single-flight, Mutex held only during map ops, refresh-by-namespace on dialog open), validate_candidate (parse errors coexist with field violations), and two new Tauri commands; comprehensive tests cover cache hits, refresh, single-flight, ungoverned no-op, and the three-field violation scenario.
quilt-sync/ui/src/pages/commit.rs Adds live debounced validation (400ms), per-field dirtiness gating, self-keyed stale-response protection, effective_metadata parity with UserMeta::Keep, and the Suspense→Transition fix that prevents JSON editor unmount on every validation refetch.
quilt-sync/ui/src/commands.rs Adds UI-side mirror types (ViolationField, CommitViolation) with matching serde attributes and byte-identical wire-form tests anchored against the backend's serialization literals.
quilt-rs/src/installed_package.rs Adds workflow_rules() that reuses existing config-fetch helpers and delegates to fetch_workflow_rules, returning Ok(None) for ungoverned packages; mirrors the commit gate's own fetch path.
quilt-sync/ui/js/json-editor-glue.js Dispatches a synthetic input event after programmatic textarea updates so the Leptos metadata_text signal tracks JSON-editor edits that wouldn't otherwise fire a native input event.
quilt-sync/src-tauri/src/model.rs Adds get_workflow_rules to the QuiltModel trait, wrapping InstalledPackage::workflow_rules to allow mock injection in unit tests.
quilt-sync/ui/assets/css/pages/commit.css Adds .qui-field-violations styling (error-palette colour, small font, no list markers) consistent with the existing workflow-selector error style.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CommitPage as commit.rs (UI)
    participant Debounce as Debounce Effect (400ms)
    participant Resource as LocalResource (validation)
    participant Backend as Tauri Backend
    participant Cache as WorkflowRulesCache
    participant QuiltRS as quilt-rs (validate_candidate_fields)

    User->>CommitPage: types in message / metadata field
    CommitPage->>CommitPage: mark field dirty, update signal
    CommitPage->>Debounce: live_key changes — arm timer
    Note over Debounce: cancel previous timer, wait 400ms
    Debounce->>Resource: debounced_key.set(key) triggers re-run

    Resource->>Backend: load_workflow_rules(ns, workflow_id, refresh)
    Backend->>Cache: ensure_loaded()
    alt cache miss
        Cache->>QuiltRS: InstalledPackage::workflow_rules()
        QuiltRS-->>Cache: WorkflowRules (compiled)
        Cache-->>Backend: rules cached in Arc OnceCell
    else cache hit
        Cache-->>Backend: no I/O
    end
    Backend-->>Resource: Ok(has_rules)

    Resource->>Backend: validate_commit_candidate(ns, wf, msg, meta, name)
    Backend->>Cache: validate() — pure cache read, no I/O
    Cache->>QuiltRS: validate_candidate_fields(rules, candidate)
    QuiltRS-->>Cache: Ok / Rejected(violations)
    Cache-->>Backend: Vec CommitViolation
    Backend-->>Resource: Vec CommitViolation

    Resource->>CommitPage: live_violations memo (self-keyed, stale responses discarded)
    CommitPage->>CommitPage: displayed_violations() — filter by field dirtiness
    CommitPage->>User: render per-field violation list (advisory, buttons stay enabled)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CommitPage as commit.rs (UI)
    participant Debounce as Debounce Effect (400ms)
    participant Resource as LocalResource (validation)
    participant Backend as Tauri Backend
    participant Cache as WorkflowRulesCache
    participant QuiltRS as quilt-rs (validate_candidate_fields)

    User->>CommitPage: types in message / metadata field
    CommitPage->>CommitPage: mark field dirty, update signal
    CommitPage->>Debounce: live_key changes — arm timer
    Note over Debounce: cancel previous timer, wait 400ms
    Debounce->>Resource: debounced_key.set(key) triggers re-run

    Resource->>Backend: load_workflow_rules(ns, workflow_id, refresh)
    Backend->>Cache: ensure_loaded()
    alt cache miss
        Cache->>QuiltRS: InstalledPackage::workflow_rules()
        QuiltRS-->>Cache: WorkflowRules (compiled)
        Cache-->>Backend: rules cached in Arc OnceCell
    else cache hit
        Cache-->>Backend: no I/O
    end
    Backend-->>Resource: Ok(has_rules)

    Resource->>Backend: validate_commit_candidate(ns, wf, msg, meta, name)
    Backend->>Cache: validate() — pure cache read, no I/O
    Cache->>QuiltRS: validate_candidate_fields(rules, candidate)
    QuiltRS-->>Cache: Ok / Rejected(violations)
    Cache-->>Backend: Vec CommitViolation
    Backend-->>Resource: Vec CommitViolation

    Resource->>CommitPage: live_violations memo (self-keyed, stale responses discarded)
    CommitPage->>CommitPage: displayed_violations() — filter by field dirtiness
    CommitPage->>User: render per-field violation list (advisory, buttons stay enabled)
Loading

Reviews (2): Last reviewed commit: "Rename validation_name to validation_han..." | Re-trigger Greptile

@fiskus
fiskus marked this pull request as ready for review July 10, 2026 11:20
Comment thread quilt-sync/ui/src/pages/commit.rs Outdated
Comment thread quilt-sync/src-tauri/src/commands/commit_data.rs Outdated
fiskus added 3 commits July 10, 2026 13:34
…etches

The live-validation LocalResource is read inside the Commit page's Suspense
boundary, so each debounced refetch re-armed the boundary's pending set and a
plain Suspense unmounted the whole CommitContent subtree (including the JSON
editor container) to show its fallback, detaching the editor's DOM and blurring
it on every keystroke. Switch the boundary to Transition so it keeps the
already-rendered children mounted while later resource loads are pending; the
editor now mounts once per dialog open and keeps focus through validation
round-trips. Initial load still shows the spinner fallback once.
The commit path validates every field regardless of whether the
metadata parses; the advisory path now does the same, dropping only the
misleading schema check that would have run against {} in place of the
unparseable text.
The namespace doubles as the full package handle that handle_pattern
matches; the old name invited swapping in a narrower value.
@fiskus

fiskus commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@greptileai Please re-review and update the confidence score. Since your review: the metadata parse error no longer swallows co-present message/handle violations (187f2f8, with a three-violation test), validation_name is renamed to validation_handle with the namespace-is-handle identity documented (001deb5), and a user-reported focus-loss bug was fixed — the commit page's Suspense boundary was unmounting the JSON editor on every validation refetch; it is now a Transition that keeps children mounted across refetches (ba05ff9).

@fiskus
fiskus merged commit d85a99b into main Jul 10, 2026
4 checks passed
@fiskus
fiskus deleted the commit-dialog-live-validation branch July 10, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant