Skip to content

Validation Protocol: Cross-Field, Async, and Conditional validation - #59

Merged
huangyiirene merged 2 commits into
mainfrom
copilot/enhance-validation-protocol
Jan 21, 2026
Merged

huangyiirene merged 2 commits into
mainfrom
copilot/enhance-validation-protocol

Conversation

Copilot AI commented Jan 21, 2026

Copy link
Copy Markdown
Contributor

Implements comprehensive validation protocol enhancements: cross-field validation for multi-field business rules, async validation for remote checks, and conditional validation for context-aware rules.

Changes

Cross-Field Validation (17 tests)

  • Date range validation (end_date > start_date)
  • Amount comparisons with multiple fields
  • Complex expressions with AND/OR operators
  • Documented with Salesforce validation rule equivalents
{
  type: 'cross_field',
  name: 'discount_limit',
  condition: 'discount > (amount * 0.40)',
  fields: ['discount', 'amount'],
  message: 'Discount cannot exceed 40% of the amount'
}

Async Validation (14 tests)

  • Remote uniqueness checks via API endpoints
  • Timeout and debounce configuration
  • Seven documented use cases: email/username uniqueness, tax ID validation, credit card verification, address validation, domain availability, coupon validation
{
  type: 'async',
  name: 'unique_email',
  field: 'email',
  validatorUrl: '/api/users/check-email',
  debounce: 500,
  timeout: 3000
}

Conditional Validation (14 tests)

  • "Validate only if X" patterns with when/then/otherwise
  • Nested conditionals for complex business logic
  • Role-based and region-specific validation
{
  type: 'conditional',
  when: 'order_total > 10000',
  then: {
    type: 'script',
    name: 'manager_approval_required',
    condition: 'manager_approval_id = null'
  }
}

Documentation

  • Module-level docs comparing ObjectStack to Salesforce patterns
  • Inline examples for each validation type
  • Best practices for async validation (timeouts, debouncing, caching)
Original prompt

Week 3: Validation Protocol Enhancement

Day 1-2: Cross-Field Validation

Define CrossFieldValidationSchema in validation.zod.ts
Support expressions like "end_date > start_date"
Write 15+ tests
Document with Salesforce validation rule examples
Day 3-4: Async Validation

Define AsyncValidationSchema in validation.zod.ts
Support remote uniqueness checks via API
Write 10+ tests with mock endpoints
Document use cases (email uniqueness, username availability)
Day 5: Conditional Validation

Add condition field to ValidationSchema
Support "validate only if X is true" patterns
Write 10+ tests


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel

vercel Bot commented Jan 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Error Error Jan 21, 2026 1:52pm

Request Review

…umentation

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
@huangyiirene
huangyiirene marked this pull request as ready for review January 21, 2026 13:55
Copilot AI changed the title [WIP] Add validation protocol enhancements for cross-field and async checks Validation Protocol: Cross-Field, Async, and Conditional validation Jan 21, 2026
Copilot AI requested a review from huangyiirene January 21, 2026 13:56
@huangyiirene
huangyiirene merged commit ae00042 into main Jan 21, 2026
9 of 10 checks passed
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…tack-ai#7768) (objectstack-ai#7813)

* feat(spec): Field.number gains useGrouping presentation hint (objectstack-ai#7768)

FieldSchema gains an optional `useGrouping: boolean` (Option A, ruled
2026-08-11 on objectstack-ai#7768, maintainer veto window open) so an authored number
field can opt out of Intl.NumberFormat's digit grouping without losing
numeric semantics -- the fix for years (Field.number({ scale: 0, min: 1900 }))
rendering as "2,026" that downstream apps have worked around three times by
converting to Field.text (hotcrm-heimao#35/objectstack-ai#40/objectstack-ai#59).

No default is declared: absent defers to the renderer (interim heuristic
today, locale default eventually -- objectui#4033's contract, not this
package's). Threads through Field.number(...) automatically via the
existing FieldInput shape, same as scale/min.

Also: liveness ledger classifies the key `planned` (objectui#4033 is the
pending consumer); authorable-surface/data.json, field.mdx and
state-counts.md regenerated to match.

* chore(spec): regenerate field docs/authorable-surface/liveness after main merge

Wholesale regen (gen:docs, gen:schema's authorable-surface projection,
gen:liveness-counts) to re-materialize artifacts that drifted from commits
main picked up since this branch's last merge — the `internal` field key
(objectstack-ai#7728) and the `flows` translation surface's planned entries (objectstack-ai#7763).
check:generated: 13/13 green; check:liveness: green.

---------

Co-authored-by: Claude <noreply@anthropic.com>
zhuangjianguo pushed a commit that referenced this pull request Sep 8, 2026
…aggregate × field-type table (#16685)

Decision batch #80 (2026-09-08) holds ruling #11152 - booleans aggregate as
numbers on every backend, no per-aggregate exception - over batch #59's
blanket "every other pair refused", which never named booleans. The four
arithmetic / order rows gain the BOOLEAN_VALUE_TYPES members; the module
TSDoc and the table's pending changeset no longer publish the refusal; the
pins hold the boolean rows both literally and against AGGREGATION_CASES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…AggregationFunction × FieldType) dataset measures are refused against (objectstack-ai#16353) (objectstack-ai#16684)

* feat(spec): declare the aggregate × field-type compatibility matrix (objectstack-ai#16353)

Export AGGREGATE_FIELD_TYPE_COMPATIBILITY and isAggregateCompatibleWithFieldType
from @objectstack/spec/data: the one table the dataset compiler and the lint
rule refuse dataset measures against. Rows follow the director ruling
(decision batch objectstack-ai#59), resolved against the full FieldType membership through
the field-value semantic classes; pinned literally in the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf

* chore(spec): pin the aggregate × field-type table in the api-surface and export-origins baselines (objectstack-ai#16353)

Regenerated by `check:generated --fix` after a full spec build: the two
stale shards (api-surface/data.json, export-origins/data.json) each gain the
two new exports and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf

* fix(spec): fail-closed shape guard on isAggregateCompatibleWithFieldType; correct the published grounds for the boolean and time rows (objectstack-ai#16353)

Contract-review patch round. The predicate now refuses any non-string input
(a property-key lookup alone coerced ['count'] / { toString } to a member
spelling); pinned. The TSDoc and changeset no longer claim booleans are the
divergence class - objectstack-ai#11152 has every backend answer them as numbers - and
record that row, plus the min/max refusal over the string classes (objectstack-ai#15768
types them as a supported 'string' result), as overrides of existing
opinions referred to the maintainer. The time justification names SQLite's
canonical TEXT form (objectstack-ai#3994). No row changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8SRGcf2eKTK7RRpWCGxwf

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…aggregate × field-type table (objectstack-ai#16685) (objectstack-ai#16750)

* feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (objectstack-ai#16685)

Decision batch objectstack-ai#80 (2026-09-08) holds ruling objectstack-ai#11152 - booleans aggregate as
numbers on every backend, no per-aggregate exception - over batch objectstack-ai#59's
blanket "every other pair refused", which never named booleans. The four
arithmetic / order rows gain the BOOLEAN_VALUE_TYPES members; the module
TSDoc and the table's pending changeset no longer publish the refusal; the
pins hold the boolean rows both literally and against AGGREGATION_CASES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x

* docs(spec): retract the d.ts byte-identity claim in the boolean-members changeset; derive the flag-case vocabulary pin from AggregationFunction (objectstack-ai#16685)

Contract-review patch round. The changeset claimed dist/*.d.ts was
byte-identical; it is not - the rewritten module TSDoc ships in
dist/data/index.d.ts. It now states what holds: the exported declarations
are unchanged, the private BOOLEAN_AGGREGATE_FIELD_TYPES constant is absent
from the bundle, and api-surface / export-origins are untouched. The test
header claims only what the cross-pin reaches (the boolean axis) and the
flag-case vocabulary is derived from AggregationFunction.options instead of
a literal six-member list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x

* docs(spec): state the boolean-members constant's absence as measured — dist/*.d.ts and the bundles' export lists, not the bundle (objectstack-ai#16685)

Contract-review patch round 2. The constant does ship inside the bundles as
a non-exported binding, so "absent from the bundle" over-claimed; the
changeset now says only what was measured: absent from dist/*.d.ts and
from the bundles' export lists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x

---------

Co-authored-by: Claude <noreply@anthropic.com>
os-bill pushed a commit that referenced this pull request Sep 10, 2026
…at it

Amendment by addition, in ADR-0058's own idiom: a new blockquoted block after
Amendment II.2 recording the maintainer ruling (decision batch #59, 2026-09-06)
that per-row `previous` on a predicate write may serve a row-invariant-in-effect
rewrite, with MULTI_UPDATE_HOOK_KEY_DIVERGENCE (#14099) as the engine mechanism
that makes it safe and the two shapes the rule does not admit.

The superseded 2026-08 D3 sentence is left standing as the dated record and
carries a forward pointer to the new block, so the AGENTS.md directive-13 grep
lands on the pointer at the line that would otherwise read as the live rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
os-bill pushed a commit that referenced this pull request Sep 10, 2026
…view F1-F4)

Four prose corrections from the delta contract review's non-blocking findings.
No behaviour change, no contract change, no new argument.

F1 — ADR-0058 Amendment II.3's ruling paragraph cited the date and the decision
batch but not the recording comment. It now names comment `5560086928`, the
comment on this card that records the maintainer reply the block quotes.

F2 — round 3's rewrap left a stub line (`matched). So an`) mid-paragraph. The
paragraph is rewrapped to the block's own idiom; the prose is word-identical.

F3 — the block said it amends D3's closing SENTENCE. It amends the bullet's
last two: the "rewrite *conditioned* on the row is out of contract" sentence is
superseded for the in-place / same-key-set case alongside the "not so a rewrite
can be aimed" one. Now "closing sentences".

F4 — the changeset attributed the ruling to the director seat. The MAINTAINER
ruled; the director seat recorded it. This text ships to consumers inside the
package's CHANGELOG.md, so the misattribution was published. Now "Maintainer
ruling (recorded by the director seat, decision batch #59, 2026-09-06)".

Level re-derived rather than inherited: `packages/spec`'s files[] carries
src/**/*.zod.ts and dist, while docs/adr/** is in no package's files[]. This
round moves no published carrier and changes nothing behavioural, so the
existing `@objectstack/spec: minor` stands.

Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants