FS FormScript
Package@formscript/validation
Specification4.8, 10, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6
Depends onast, decimal, errors, evaluator, types

@formscript/validation

The validation model of the FormScript Language Specification 1.0.0, §10.

The package is pure: it judges a field's constraints and a form's validity from the values it is given. It never fetches, never schedules and never reads the clock, so two conforming runtimes reach the same verdict for the same inputs.

Specification sections implemented

Section What this package implements
§10.1 validateOn — blur | change | submit | blur+submit, default blur
§10.2 Constraint evaluation, constraint accumulation (Spec-10.5), custom (Spec-10.6, Spec-10.7)
§10.2.1 accepted (Spec-10.9, Spec-10.10), required on a boolean (Spec-10.8)
§10.3 cross and the other cross-field constraints (Spec-10.12)
§10.4 Form-level validity (Spec-10.13 to Spec-10.17) and the "do not show a violation too early" rule (Spec-10.4)
§10.5 Message severity: error by default, warn for a warning (Spec-10.18)
§10.6 Satisfiability analysis, tiers A1 (MUST) and A2 (SHOULD) (Spec-10.19 to Spec-10.29)

@formscript/types owns the field-state lattice; this package uses deriveValidity, mayShowViolation and satisfiesRequired rather than re-deriving them.

Public API

Model

ts
type ValidateOn = 'blur' | 'change' | 'submit' | 'blur+submit';
const VALIDATE_ON_VALUES: readonly ValidateOn[];
const DEFAULT_VALIDATE_ON: ValidateOn;            // 'blur'

interface ConstraintSpec {
  readonly severity: 'error' | 'warning';
  readonly constraint: Constraint;                 // @formscript/ast
  readonly span: SourceSpan;
}

function collectConstraints(block: Block): readonly ConstraintSpec[];
function validateOnOf(block: Block): ValidateOn;
function validatesOn(validateOn: ValidateOn, event: 'blur' | 'change' | 'submit'): boolean;
function isUnguarded(constraint: Constraint): boolean;

collectConstraints also expands the required property into its required constraint (Spec-8.29).

Runtime

ts
type ViolationCode = 'E-VAL-01' | 'E-VAL-02' | 'E-VAL-03' | 'E-VAL-04' | 'E-VAL-05';

interface Violation {
  readonly code: ViolationCode;
  readonly severity: 'error' | 'warning';
  readonly message: string;
  readonly messageKey: string;        // a catalogue key, for localisation
  readonly kind: Constraint['kind'];
  readonly path: readonly string[];
  readonly span: SourceSpan;
}

interface ValidationEnvironment {
  evaluate(expression: Expression, context: ConstraintContext): unknown;
  read(path: readonly string[]): unknown;
  peers(path: readonly string[]): readonly unknown[];
}

function evaluateField(input: FieldValidationInput, environment: ValidationEnvironment): FieldValidationResult;
function guardOf(constraint: Constraint): Expression | undefined;
function compareDataPlane(left: unknown, right: unknown): number | null;
function scalarLength(text: string): number;

evaluateField returns the field's validity, every failing constraint (Spec-10.5), the deferred constraints (E-VAL-05), whether a violation may be shown (Spec-10.4) and whether an error value reached a constraint.

Form level

ts
function judgeForm(input: FormValidationInput): FormValidationResult;

FormValidationResult carries valid (the $form.valid judgement, Spec-10.17), submittable (the strict-aware judgement, Spec-10.14), firstFailing (the first failing field in document order, Spec-10.16), invalidFields, pendingFields, violations and deferred.

Satisfiability

ts
function analyzeSatisfiability(document: Document, options?: SatisfiabilityOptions): SatisfiabilityReport;
function findingsToDiagnostics(findings: readonly SatisfiabilityFinding[]): Diagnostic[];
function analyzePattern(pattern: string): PatternInfo;

The analysis is a decision procedure over domains, not a solver: no search, no backtracking and no value enumeration (Spec-10.20). The conclusion of every field follows from the rules of §10.6.1 and §10.6.2, so two conforming implementations agree.

Judgement calls

The specification leaves the following open. Each choice is noted here rather than hidden in the code.

  1. A value-based constraint on an empty field passes. min, max, minLength, maxLength, pattern, email, phone, url, in, notIn, multipleOf, equal, notEqual, cross and unique are vacuously satisfied when the field holds nothing. Only required (and accepted) inspect emptiness. The alternative — treating an absent value as an undetermined comparison — would make every optional field with a min constraint pending, which contradicts required existing as a separate constraint.
  2. required = false is a constraint guarded by the literal false. Spec-8.29 says "required = <bool literal> is exactly validate { required }", which read literally would make required = false mean required. Spec-8.31 ("required = false with accepted is the normal spelling") shows that the intended meaning is "not required", so required = false is modelled as a constraint whose guard is the literal false and therefore never binds (Spec-5.10).
  3. An absent bound defers the constraint. When the bound of min, max, minLength, maxLength or multipleOf evaluates to null or inapplicable, the constraint is deferred (E-VAL-05) rather than silently passing, so the deferral is visible rather than silent.
  4. custom returning anything other than true, null or a string fails. Spec-10.6 lists only those three results. false and inapplicable are therefore treated as failures with the default message; the error value aborts the enclosing transaction (E-EVAL-07).
  5. The email, phone and url grammars are this implementation's choice. §4.8 names the three constraints but fixes no grammar. The patterns are conservative and locale-independent (Spec-12.9). A phone must also carry between 7 and 15 digits.
  6. The length of a string is its number of Unicode scalar values of the NFC form (Spec-5.35), and the scalar count is used for minLength and maxLength.
  7. $form.valid is the base judgement, not the strict-aware one. Spec-10.17 says $form.valid evaluates to the result of Spec-10.13, but Spec-10.14 makes Spec-10.13 strict-dependent and Spec-10.15 forbids strict from affecting any data-plane result. $form.valid is therefore "no field is invalid" (Spec-5.13) and never depends on strict; submittable is the strict-aware judgement.
  8. The E-BOUND-* codes are not re-reported. Spec-10.22 gives the E-BOUND-* codes precedence over E-SAT-01, but @formscript/analyzer already reports them. This package suppresses E-SAT-01 and E-SAT-02 for a field that has an E-BOUND-* finding, so the more specific code wins without the diagnostic being emitted twice.
  9. Tier A2 narrows guards only as far as the rules allow. A guard is treated as binding only when it is absent or the literal true (Spec-10.25). Narrowing a guard to a value it may or may not take would require enumerating the value space, which Spec-10.20 forbids; such a field is therefore undetermined at tier A2 and is not reported.
  10. W-SAT-01 is also reported for a field inside an entity whose when is the literal false. The entity's own when makes the field unreachable by Spec-8.2, so the field is provably dead in exactly the sense Spec-10.28 describes.
  11. A constraint message carries a messageKey. Spec-10.10 requires the accepted message to be localisable (Spec-12.7); every violation therefore carries a stable key next to its message so a host can localise it.

Overlap with @formscript/analyzer

The analyzer reports the static half of §10.6 — it already emits E-SAT-01 for an enum whose option set is disjoint from the declared options, and the E-BOUND-*, W-VAL-01 and E-CYCLE-01 codes. This package does not duplicate those diagnostics. Its tier A1 domain intersection is a superset of the analyzer's enum/options check: it also reports E-SAT-01 for a pattern that cannot match inside the declared length range, for a notIn that empties an enum, for a multipleOf whose interval holds no multiple, and for a required that removes the only value of the domain. A host should not run both reporters without deduplicating.

Public API

44 exported symbols.

acceptableLengths function

The lengths a string domain accepts, after the patterns are applied.

analyzePattern re-export
analyzeSatisfiability re-export
collectConstraints re-export
compareDataPlane re-export
ConstraintContext interface

The context a constraint is evaluated in.

ConstraintSpec interface

A constraint together with the severity of its message (Spec-10.18).

DEFAULT_VALIDATE_ON re-export
Domain interface
entityAt function

The entity a path names, or `null`.

evaluateField re-export
FieldValidationInput interface
FieldValidationResult interface
findingsToDiagnostics re-export
FORMAT_PATTERNS const

The `email`, `phone` and `url` grammars of §4.8; the specification fixes none, so these are this implementation's choice.

FormFieldValidation interface
FormValidationInput interface
FormValidationResult interface
guardOf re-export
hasE164DigitCount function

Whether a string has between 7 and 15 digits, the E.164 count.

intersectDomains function

The intersection of two domains (Spec-10.21).

isEmptyDomain function

Whether the domain is provably empty (Spec-10.23, Spec-10.24).

isUnguarded re-export
judgeForm function

Judges the form as a whole; `valid` never depends on `strict` (Spec-10.15).

LengthRange interface
NumericRange interface
PatternInfo interface

The length analysis a `pattern` constraint needs for tier A1 (Spec-10.23); it enumerates no values (Spec-10.20).

patternSource function

A pattern's analysis, or `null` when the pattern cannot be used.

PatternSource interface
SatisfiabilityCode type
SatisfiabilityFinding interface
SatisfiabilityOptions interface
SatisfiabilityReport interface
scalarLength re-export
typeDomain function

The domain a declared type admits (Spec-10.21).

UNRESTRICTED const
VALIDATE_ON_VALUES re-export
ValidateOn type

When a field's constraints are evaluated (§10.1).

validateOnOf re-export
validatesOn re-export
ValidationEnvironment interface

What a runtime supplies to the validator.

ValidationEvent type

The events that can trigger validation.

Violation interface

A message that failed a constraint (Spec-10.5).

ViolationCode type