AI Integration

FriendlyText

Human-readable labels and error messages for a type.

Experimental The enrichment workflow works today. The generated file shape and the commands may still change between minor versions.

FriendlyText<T> gives every field of a type a label and error messages. You commit the map next to your type, and the compiler checks it against that type on every build. Use it to turn validation errors into messages people can read.

FriendlyText Shape

The map mirrors your type, every field included. Every node is { rt$label, rt$errors, ...fields }: both rt$ keys always, then one key per child field. Arrays use rt$items, tuples rt$slots, a Map rt$keys and rt$values, a Set rt$values.

import type * as TF from '@mionjs/run-types/formats';

// src/models/user.ts
export interface User {
  name: TF.String<{minLength: 2; maxLength: 60}>;
  age: TF.Number<{min: 0; max: 120}>;
  isActive: boolean;
  tags: string[];
  profile: {
    email: TF.Email;
    score: TF.Number<{min: 0; max: 100}>;
  };
}
A blank string means "no custom text here" and renders the fallback, but --require-complete still fails on it. Never delete a key: the next sync adds it back.

Keeping FriendlyText in Sync

When your type changes, enrich --update re-syncs the map and keeps every label and message you wrote. Text an edit would throw away is kept as a comment:

export const friendlyUser: FriendlyText<User> = {
  rt$label: 'User account',
  rt$errors: { type: '' },
  name: { rt$label: 'Full name', rt$errors: { type: '' } },
  /* @rtOrphanChild isActive: { rt$label: 'Active?', rt$errors: { type: '' } }, */   // you removed isActive
};
The editWhat happens to your text
Rename a field or a whole typeit follows the field, untouched
Remove a fieldits node is kept as a comment marked @rtOrphanChild
Change a field's typethe label always survives, and only the messages the new type no longer declares are kept as comments
Change a field to a different shape (a number becomes an array of objects)the whole old node is kept as a comment and a fresh blank takes its place
Delete a whole typeits entire map is kept as a comment marked @rtOrphan

Pruning Comments Left by Removed Fields

Comments left by a removed field or type stay until a prune clears them all at once.

mion enrich --prune
Prune removes only the @rtOrphan and @rtOrphanChild comments, never a live field or a @todo blank. Bring a removed field or type back and the next enrich --update restores its text.
MockData<T> works the same: a removed field's sample pool is kept as a comment until enrich --prune.

Error Message Keys

Each rt$errors key names the rule the value broke, and params that never fail (such as float, isCurrency and the transform block) get no key. Your editor accepts only keys the field's format can fail on, so a typo shows right away. mion enrich --no-emit warns about a missing key (enrich-text-missing-message) or one the field can never fail on (enrich-text-unknown-error-key), and --update adds missing keys as blanks.

Failurert$errors key$[val] resolves to
base type-shape (wrong kind)typen/a
string minLength / maxLengthminLength / maxLengththe bound (2, 60)
string patternpatternn/a
number min / max (minimum / maximum)min / maxthe bound
number lt / gt (exclusiveMaximum / exclusiveMinimum)lt / gtthe bound
number integerintegertrue
date or time in the wrong layoutformatthe layout ('ISO')
date-time date / time partdate / timen/a
uuid or IP versionversion'4'
credit card format or checksumcreditCardn/a
credit card network not allowednetworksthe allowed list
email address (EmailAddress, IdnEmail)emailRfcn/a
email with no @@n/a
host name (Hostname, IdnHostname)idnan/a
domain label starts or ends with -hyphenn/a
Errors accumulate: a value that breaks both minLength and pattern gets one message per broken rule. For one message per field, use rt$default below.

Message Placeholders

The compiler checks these $[…] tokens and the renderer fills them in.

TokenResolves to
$[label]the node's rt$label, falling back to the raw field name
$[val]the failed constraint's bound (e.g. 2 for minLength: 2)
$[path]the dotted path to the field (profile.email)
$[index]the array element index, for rt$items failures
$[value] (the received value) is not supported yet: the error does not carry the input.

Plural Messages

A count rule (minLength, maxLength, min, max, lt, gt) can hold one wording per plural form instead of a single string. The form follows the broken limit (the 3 in minLength: 3), and only other is required.

minLength: {
  one: '$[label] needs at least $[val] character',
  other: '$[label] needs at least $[val] characters',
},

The scaffold has the forms each file's language needs (English gets one and other, see Translations (i18n)). Other rules (type, pattern, and the rest) stay a single string, and a single string still works on a count rule when one wording fits every count.

One Message per Field

Make rt$default the only key in rt$errors and it becomes the message for every failure of that field. A node has either rt$default or the per-rule keys, never both, so each field picks its own mode. It is translatable like any other message.

import type * as TF from '@mionjs/run-types/formats';
import type {FriendlyText} from '@mionjs/run-types';

interface Signup {
  name: TF.String<{minLength: 2; maxLength: 60}>;
}

export const friendlySignup: FriendlyText<Signup> = {
  rt$label: 'Signup',
  rt$errors: {type: ''},
  name: {
    rt$label: 'Full name',
    rt$errors: {rt$default: 'Enter a name between 2 and 60 characters'},
  },
};
New nodes are always scaffolded with per-rule keys. Switch a node to rt$default by hand and every later sync keeps it.

Rendering Messages

createFriendlyText<T>(map) returns a renderer with two methods. It needs only the map and the errors.

import {
  createFriendlyText,
  createGetValidationErrorsFn,
} from '@mionjs/run-types';
import type {User} from './user';
import {friendlyUser} from './friendly-user';

const getUserErrors = createGetValidationErrorsFn<User>();
const friendly = createFriendlyText<User>(friendlyUser);

// label(path): dotted string or a raw path-segment array
friendly.label('profile.email');
// → 'Email'

// errors(errs): render a createGetValidationErrorsFn result into messages
const badInput: unknown = {name: 'A', age: 200, profile: {email: 'nope'}};
friendly.errors(getUserErrors(badInput));
// → [{ path: 'profile.email', label: 'Email', message: 'Enter a valid email address' }, …]

errors() groups failures by path, picks the message for each broken rule and fills in its tokens. Each result is a FriendlyMessage:

interface FriendlyMessage {
  path: string;     // dotted path to the field ('profile.email'); '' for the root
  label: string;    // the field's rt$label, or its raw last path segment as fallback
  message: string;  // the interpolated message
}

A blank or missing message falls back to "

Error rendering works today. Form-building UI is coming later.
Copyright © 2026