diff --git a/docs/docs/design/type-bounds.md b/docs/docs/design/type-bounds.md index 2a2afc3..324a540 100644 --- a/docs/docs/design/type-bounds.md +++ b/docs/docs/design/type-bounds.md @@ -1,6 +1,7 @@ # Bounds on types -Status: **accepted 2026-11-02; steps 1–6 shipped, 7–12 in progress** · +Status: **accepted 2026-11-02; steps 1–10 shipped, except step 10's +worst-case sizes against configured limits; 11–12 in progress** · [core#98](https://github.com/ComlineProject/core/issues/98) · Decided through [the proposals](../proposals/type-bounds/index.md): [Proposal D](../proposals/type-bounds/max-only.md), with @@ -112,8 +113,10 @@ receivers. Adopting a maximum is breaking too, accepted before 1.0. ### Validators -- **Enforced by default.** `validate_on_decode` defaults to checking; turning it - off is an explicit give-up with a reason. +- **Enforced by default.** `validate_on_decode` defaults to `collect`; turning it + off is an explicit give-up with a reason, `validate_on_decode = { off = "why" }` + in `comline.toml`. A bare `"off"` is refused, and the give-up shows in + `comline generate` and `comline inspect`. - **std gains** `NonEmpty`, `MinChars`, `MinItems` and `IntRange`. `StringBounds.max_chars` defaults to `u32::MAX`, then is deprecated. - **Generators build the std minimum validators into the decoder,** at a @@ -172,6 +175,6 @@ runtime core primitive is used only where measurement shows it's faster. | 7 | runtime | conformance vectors for bounds per format, the depth limit, and bounds in the compatibility facts | | 8 | comline-rust | checks while decoding and encoding, alias newtypes, validators on by default | | 9 | comline-typescript | checks while decoding and encoding, branded aliases; validators with core#96's step 4d | -| 10 | cli | `validate_on_decode` default, worst-case sizes against configured limits, migration | +| 10 | cli | `validate_on_decode` default, worst-case sizes against configured limits, migration. **Shipped:** `validate_on_decode` defaults to `collect`, and off is `{ off = "reason" }`; the migration (the build lists each unbounded position, shows the `settings` block to add, and offers to write it from the author's input); the pin bump. **Left:** checking worst-case sizes against configured limits | | 11 | language-server | hover sizes and cost per target, the nested-bounds lint | | 12 | docs | the guide: bounds, `char`, and the migration | diff --git a/docs/docs/design/validators.md b/docs/docs/design/validators.md index a7d9fdc..fd44653 100644 --- a/docs/docs/design/validators.md +++ b/docs/docs/design/validators.md @@ -249,7 +249,7 @@ left to right, parenthesising each comparison. |---|---| | 4a | **Done (core#108).** Freeze the `validate` condition as a structured expression tree (keep the text + `references` the checks use). | | 4b | **Done for Rust.** `rust` generator emits `validate()` (collect-all) and `validate_first()` (fail-fast); stand up the `comline_rt` support crate with `ValidationError` / `ValidationErrors`; implement the `value.*` accessor set. | -| 4c | **Done for Rust.** `comline.toml` `validate_on_decode` knob (`off` / `collect` / `first`) — `#[serde(try_from)]` shim in the `rust` target; default `off`. [Sketch below](#auto-validation-on-decode-4c). | +| 4c | **Done for Rust.** `comline.toml` `validate_on_decode` knob (`off` / `collect` / `first`) — `#[serde(try_from)]` shim in the `rust` target; default `off` then; `collect` since bounds step 10 ([core#98](https://github.com/ComlineProject/core/issues/98)), with off given up as `{ off = "reason" }`. [Sketch below](#auto-validation-on-decode-4c). | | 4d | The other generators (python / typescript / luau) once they exist; imported / cross-schema validators (needs the imported schema's IR at generate time, like multi-version `generate`). | ### Auto-validation on decode (4c) diff --git a/docs/docs/guide/idl/validator.md b/docs/docs/guide/idl/validator.md index b551d12..53c14ad 100644 --- a/docs/docs/guide/idl/validator.md +++ b/docs/docs/guide/idl/validator.md @@ -220,10 +220,12 @@ Each `ValidationError` carries `field`, the path to the failing value filled in (`friends[1] must be at least 2 characters`). An absent `optional` field skips its checks. -Nothing calls `validate()` for you unless you ask: -[`validate_on_decode`](../../reference/congregation/comline-toml.md#validate_on_decode) -in `comline.toml` makes decoding run the checks, and makes a provider refuse a -call whose arguments fail them. +Decoding runs the checks for you by default (`validate_on_decode = "collect"` +in `comline.toml`; `"first"` stops at the first failure), and a provider +refuses a call whose arguments fail them. Turning that off is a give-up with +its reason, `validate_on_decode = { off = "why" }`; then nothing runs until you +call `validate()`. See +[`validate_on_decode`](../../reference/congregation/comline-toml.md#validate_on_decode). Properties are filled in when the code is generated: the argument you wrote, else the property's default, with constants resolved. Nothing about the diff --git a/docs/docs/guide/ir/cas.md b/docs/docs/guide/ir/cas.md index da00393..3d491bd 100755 --- a/docs/docs/guide/ir/cas.md +++ b/docs/docs/guide/ir/cas.md @@ -369,8 +369,10 @@ A ref that was already resolved by picking one side does not reconcile on its own: the build sees one head, and the other side's commits are simply not on the line. Its changes are still built from the merged working tree. To record the dropped head as well, core's `cas::reconcile::mark_diverged` puts it back -beside the current one, and the next build reconciles the two. The CLI does -not expose this yet. +beside the current one, and the next build reconciles the two. From the CLI, +`comline build --reconcile ` does both, given the dropped head's full +hash. Either way, the build says it reconciled: which head the line continued +from, and which one it recorded as the second parent. ## Compared with Git diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index c944d4e..68040df 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -36,7 +36,7 @@ parse → resolve imports → validate → (freeze into .comline/) → (generate | `comline accept [::] [--yes]` | Accept what a dependency's [`@unverified`](../guide/idl/protocol.md#giving-up-a-guarantee) protocols give up: shows each one's given-up guarantees and reason, then writes `accept_unverified` into its `config.idp` entry, leaving the rest as written. Only protocols not already accepted for exactly their current set are asked about. On a terminal you're asked (default no); without one it refuses (exit `2`) unless `--yes` (`-y`). | | `comline check` | Parse, resolve and validate. No `.comline/` write (beyond fetching git dependencies), no version bump — safe for editors, hooks, CI. | | `comline inspect [--json]` | Show each protocol's format, framing and handshake, with their parameters and where each came from (`@framing`/`@handshake`, `config.idp`, or core's default), then every guarantee, held or given up, with the cause, the `@unverified` reason and whether it's accepted. No `.comline/` write. `--json` prints the same as JSON. See [comline inspect](tools/inspect.md). | -| `comline build [--watch]` | Compile, validate, and freeze a build into `.comline/` under a pre-release version (`0.3.0-dev.2`); print the changelog, the bump, and the next release. Before freezing, proves every declared `code_generation.languages` target against this binary's generators ([Language versions](../guide/codegen/generating-code.md#language-versions)). Releasing is `comline release`. | +| `comline build [--watch] [--reconcile ]` | Compile, validate, and freeze a build into `.comline/` under a pre-release version (`0.3.0-dev.2`); print the changelog, the bump, and the next release. `--reconcile ` puts a head dropped in a Git merge back beside the current one, so the build [reconciles the two](../guide/ir/cas.md); the output says when a build reconciled. A project with [unbounded strings or lists](../guide/idl/types.md#every-string-and-list-has-a-maximum) fails with each position listed and the `settings` block to add; on a terminal, the build offers to write it with maxima you type in. Before freezing, proves every declared `code_generation.languages` target against this binary's generators ([Language versions](../guide/codegen/generating-code.md#language-versions)). Releasing is `comline release`. | | `comline generate [--target ] [--out ] [--layout ] [--mode ] [--external-std] [--no-external-std] [--validate-on-decode ] [--dry-run] [--list] [--watch]` | Validate (no freeze), then write generated code. Location/layout from [`comline.toml`](congregation/comline-toml.md); targets from there or `config.idp`. A target can use an [external generator](../guide/extending/generators.md) (`generator = ""`) and map [encodings](../guide/extending/encodings.md#5-map-it-to-a-type). | | `comline release [1.0] [--raise --reason ] [--yes]` | Build, then release: create the version from the highest bump since the last release ([builds and releases](versioning.md#builds-and-releases)). `--raise patch\|minor\|major` goes higher, with a `--reason` recorded with the release; it never lowers. `1.0` makes a `0.x` package stable after showing what that commits to (its protocols, and what's worth settling first). Raising to major and `1.0` ask on a terminal; without one they refuse (exit `2`) unless `--yes` (`-y`). | | `comline diff []` | Show schema changes between two built versions. Each argument is a version (`0.2.0`), a commit hash (4+ chars), or `HEAD` (`` default). | @@ -54,10 +54,12 @@ reference std's own package — `comline_std` for rust, `@comline/std` for typescript — instead of inlining it, and `--no-external-std` inlines it; either overrides [`comline.toml`](congregation/comline-toml.md)'s `[generate] external_std`, which itself defaults to on in `lib` mode and off -in `code` mode. `--validate-on-decode off|collect|first` overrides +in `code` mode. `--validate-on-decode collect|first` overrides [`[generate] validate_on_decode`](congregation/comline-toml.md#validate_on_decode), whether decoding runs the schema's `@validators` (also -`COMLINE_GENERATE_VALIDATE_ON_DECODE`). +`COMLINE_GENERATE_VALIDATE_ON_DECODE`). The default is `collect`; turning it +off is a give-up written in `comline.toml` with its reason +(`{ off = "why" }`), and `generate` notes it per target. Each target's files are drawn as a tree. `--list` prints them one per line instead, with the schema each came from — and, when several package versions are generated at once, the version each line belongs to. `--dry-run` writes diff --git a/docs/docs/reference/congregation/comline-toml.md b/docs/docs/reference/congregation/comline-toml.md index 75d8b2e..b0d58c7 100644 --- a/docs/docs/reference/congregation/comline-toml.md +++ b/docs/docs/reference/congregation/comline-toml.md @@ -20,7 +20,7 @@ Defaults for every target. | `mode` | `"code"` \| `"lib"` \| `"dylib"` | `"code"` | Emit form. `code` and `lib` are implemented for `rust` and `typescript`; `dylib` is planned — see [Library generation](../../guide/codegen/library-generation.md). | | `package_versions` | `"latest"` \| `"all"` \| list | `"latest"` | Which package versions to emit — see [below](#package_versions). | | `external_std` | bool | on in `lib`, off in `code` | Reference `std` as its own package instead of inlining it — see [below](#external_std). | -| `validate_on_decode` | `"off"` \| `"collect"` \| `"first"` | `"off"` | Whether decoding a value runs its [`@validators`](../../guide/idl/validator.md) — see [below](#validate_on_decode). | +| `validate_on_decode` | `"collect"` \| `"first"` \| `{ off = "why" }` | `"collect"` | Whether decoding a value runs its [`@validators`](../../guide/idl/validator.md); off is a give-up with its reason — see [below](#validate_on_decode). | | `async` | bool | `false` | Generate protocols on a connection for an async driver — see [below](#async). | | `encodings` | table | — | Which type implements each encoding (format, framing, handshake) in the generated code — see [below](#encodings). | | `only` | list of strings | — | Generate just these languages, out of the ones `config.idp` declares — see [below](#only). | @@ -147,18 +147,33 @@ whether **decoding** calls them too: | Value | Decoding a value that fails its checks | |---|---| -| `"off"` | succeeds; nothing runs until you call `validate()` | -| `"collect"` | fails, with every failed check | +| `"collect"` (the default) | fails, with every failed check | | `"first"` | fails, with the first failed check | +| `{ off = "why" }` | succeeds; nothing runs until you call `validate()` | -It's yours to choose rather than the schema's: it changes what decoding costs, -and a form that wants every problem at once and a hot path that wants to stop -early want different things from the same type. With it on, a provider also -refuses a call whose arguments fail their checks, the same way it refuses -arguments that don't decode. +Validators are enforced by default: a schema's checks hold wherever its values +are decoded. Choosing between `collect` and `first` is yours rather than the +schema's: a form that wants every problem at once and a hot path that wants to +stop early want different things from the same type. A provider also refuses a +call whose arguments fail their checks, the same way it refuses arguments that +don't decode. + +Not running them is a **give-up**, like [`@unverified`](../../guide/idl/protocol.md), +and it carries its reason: + +```toml +[generate] +validate_on_decode = { off = "the gateway validates every request first" } +``` + +A bare `"off"` is refused, with that fix. `comline generate` notes each target +that gives it up, with the reason, and `comline inspect` lists it after the +protocols (`--json`: `validate_on_decode_given_up`). Today only the `rust` generator runs validators; `typescript` ignores this. -`--validate-on-decode` and `COMLINE_GENERATE_VALIDATE_ON_DECODE` override it. +`--validate-on-decode collect|first` and `COMLINE_GENERATE_VALIDATE_ON_DECODE` +override it for one run; turning it off is written in `comline.toml`, where +its reason is kept. ### `async` diff --git a/docs/docs/schemas/comline-toml/0.x.json b/docs/docs/schemas/comline-toml/0.x.json index fc83a7d..32fbca8 100644 --- a/docs/docs/schemas/comline-toml/0.x.json +++ b/docs/docs/schemas/comline-toml/0.x.json @@ -40,7 +40,7 @@ "type": "string" }, "default": {}, - "description": "Where each dependency's documentation lives, by the name the package\ngives the dependency (or `std`): `links = { std = \"https://...\" }`. A\ntype from a dependency listed here links into its docs; one that\nisn't is plain text.", + "description": "Where each dependency's documentation lives, by the name the package\ngives the dependency (or `std`): `links = { std = \"https://...\" }`. A\ntype from a dependency listed here links into its docs; one that\nisn't links to the pages renderers write for it under `deps/`.", "type": "object" }, "out": { @@ -215,14 +215,31 @@ "type": "array" }, "validate_on_decode": { - "default": "off", - "description": "Whether decoding a value runs its `@validators` checks: `off` (the\ndefault), `collect` (fail with every failure) or `first` (fail with\nthe first). Shared by every target unless overridden.", - "enum": [ - "off", - "collect", - "first" - ], - "type": "string" + "default": "collect", + "description": "Whether decoding a value runs its `@validators` checks: `collect`\n(the default: fail with every failure) or `first` (fail with the\nfirst). Not running them is a give-up that carries its reason:\n`{ off = \"why\" }`. Shared by every target unless overridden.", + "oneOf": [ + { + "enum": [ + "collect", + "first" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "Decoding doesn't run the checks: a give-up, with its reason.", + "properties": { + "off": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "off" + ], + "type": "object" + } + ] } }, "type": "object" @@ -294,13 +311,30 @@ "type": "integer" }, "validate_on_decode": { - "description": "Whether this target's decoding runs `@validators` checks: `off`,\n`collect` or `first`. Overrides `[generate] validate_on_decode`.", - "enum": [ - "off", - "collect", - "first" - ], - "type": "string" + "description": "Whether this target's decoding runs `@validators` checks: `collect`,\n`first`, or `{ off = \"why\" }`. Overrides `[generate]\nvalidate_on_decode`.", + "oneOf": [ + { + "enum": [ + "collect", + "first" + ], + "type": "string" + }, + { + "additionalProperties": false, + "description": "Decoding doesn't run the checks: a give-up, with its reason.", + "properties": { + "off": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "off" + ], + "type": "object" + } + ] } }, "type": "object" diff --git a/docs/docs/schemas/doc-model/1.json b/docs/docs/schemas/doc-model/1.json index 1d1c61c..78c2c37 100644 --- a/docs/docs/schemas/doc-model/1.json +++ b/docs/docs/schemas/doc-model/1.json @@ -672,7 +672,7 @@ "type": "object" }, { - "description": "An annotation that changes what travels on the wire (`@framing`).", + "description": "An annotation that changes what travels on the wire (`@framing`,\n`@handshake`) or what a decoder accepts (`@max_depth`, set on the\ntype or reaching it from a `bounds.depth.max` default).", "properties": { "key": { "type": "string" @@ -1728,7 +1728,7 @@ "type": "object" }, { - "description": "`T[]` (`length: None`) or `T[N]`.", + "description": "`T[]`, `T[N]` (`length`) or `T[max N]` (`max`), at most one of the\ntwo. An array of `char` is a string: `char[max 64]` holds at most 64\ncharacters. `max` is absent when there is none.", "properties": { "element": { "$ref": "#/definitions/Type" @@ -1744,6 +1744,14 @@ "integer", "null" ] + }, + "max": { + "format": "uint64", + "minimum": 0, + "type": [ + "integer", + "null" + ] } }, "required": [ diff --git a/docs/docs/std/http.md b/docs/docs/std/http.md index 7bb58cd..a875791 100644 --- a/docs/docs/std/http.md +++ b/docs/docs/std/http.md @@ -29,10 +29,10 @@ Variants: An HTTP request: its method and target URI. -| Field | Type | -|---|---| -| `method` | [`HttpMethod`](#httpmethod) | -| `uri` | `string` | +| Field | Type | Description | +|---|---|---| +| `method` | [`HttpMethod`](#httpmethod) | | +| `uri` | `char[max 8192]` | At most 8192 characters, as most servers allow. | diff --git a/docs/docs/std/validators.md b/docs/docs/std/validators.md index a9b71a7..d23cb08 100644 --- a/docs/docs/std/validators.md +++ b/docs/docs/std/validators.md @@ -4,24 +4,98 @@ Validators to attach to fields with `@validators`. Each takes its parameters by name: - @validators = [StringBounds(min_chars = 3, max_chars = 32)] + @validators = [MinChars(min = 3)] + +A maximum belongs in the type (`char[max 32]`, `u32[max 10]`), where a +decoder checks it before allocating. These check what a type can't say: +a minimum, or a range of values. | Item | Summary | |---|---| +| validator [`NonEmpty`](#nonempty) | Checks a string or a list isn't empty. | +| validator [`MinChars`](#minchars) | Checks a string has at least `min` characters. | +| validator [`MinItems`](#minitems) | Checks a list has at least `min` items. | +| validator [`IntRange`](#intrange) | Checks an integer is within `min` and `max`, both included. | | validator [`StringBounds`](#stringbounds) | Checks a string's length is within a minimum and a maximum. | + + +## validator `NonEmpty` + +Checks a string or a list isn't empty. + +Applies to: `string`, `any[]` + +| Assert | Message | +|---|---| +| `value.length > 0` | {value.name} must not be empty | + + + +## validator `MinChars` + +Checks a string has at least `min` characters. + +Applies to: `string` + +| Parameter | Type | Description | +|---|---|---| +| `min` | `u32` | the fewest characters allowed | + +| Assert | Message | +|---|---| +| `value.length >= params.min` | {value.name} must be at least {params.min} characters | + + + +## validator `MinItems` + +Checks a list has at least `min` items. + +Applies to: `any[]` + +| Parameter | Type | Description | +|---|---|---| +| `min` | `u32` | the fewest items allowed | + +| Assert | Message | +|---|---| +| `value.length >= params.min` | {value.name} must have at least {params.min} items | + + + +## validator `IntRange` + +Checks an integer is within `min` and `max`, both included. + +Applies to: `u8`, `u16`, `u32`, `u64`, `s8`, `s16`, `s32`, `s64` + +| Parameter | Type | Description | +|---|---|---| +| `min` | `s64` = `s64::MIN` | the smallest value allowed | +| `max` | `u64` = `u64::MAX` | the largest value allowed | + +| Assert | Message | +|---|---| +| `value >= params.min` | {value.name} must be at least {params.min} | +| `value <= params.max` | {value.name} must be at most {params.max} | + ## validator `StringBounds` Checks a string's length is within a minimum and a maximum. +Deprecated in spirit: the type carries the maximum (`char[max 32]`), and +`MinChars` the minimum. `max_chars` defaults to no limit, so it never +refuses what the type allows. + Applies to: `string` | Parameter | Type | Description | |---|---|---| | `min_chars` | `u32` = `0` | minimum length of the string | -| `max_chars` | `u32` = `1024` | maximum length of the string | +| `max_chars` | `u32` = `u32::MAX` | maximum length of the string | | Assert | Message | |---|---|