← Back to Good Intentions

NTNT 0.5.0: The Verification Release

The last NTNT release was mostly about speed: stop parsing the same template, stop copying the same array, stop rebuilding the same string, stop preparing the same query. Version 0.5.0 turns to a different kind of repeated work: the edit-lint-run loop an agent enters when the language says too little, reports one error at a time, or quietly accepts code that did not verify what its author thought it verified.

NTNT 0.5.0 is the verification release. It closes the language-wide DD-063 assessment, makes every silent failure identified by that review loud, adds two standard-library modules that remove a lot of routine web-app plumbing, and hardens the multi-worker runtime. The point is not that NTNT now prevents every bad program. The point is that its tools are substantially less willing to let a broken one look finished.

Verification now means strict verification

ntnt test and ntnt intent check now run with NTNT_TYPE_MODE=strict when the environment does not set a type mode explicitly. Previously, verification could inherit the runtime's more forgiving behavior and let an invalid access degrade into None or allow an implicit conversion that production code should not depend on.

That was convenient in exactly the wrong place. A development server can reasonably offer a forgiving mode. A verification command should verify.

This is a behavior change, and upgrades may surface failures in tests or intent scenarios that passed under 0.4.12. Fixing those failures is the recommended path. Projects that deliberately depend on the old tolerance can set NTNT_TYPE_MODE=warn, but the opt-out is now visible instead of accidental.

Out-of-bounds array and string reads follow the selected mode:

  • forgiving mode returns None silently;
  • warn mode reports the problem and returns None;
  • strict mode stops with E010.

Expected misses remain easy to express. Guard the access with ??, ?, or otherwise, and NTNT treats it as deliberate. Missing map keys still return None silently because optional map fields are a normal part of application code rather than evidence that an index calculation went wrong.

The tools report more of the actual problem

A fast repair loop depends less on eloquent error prose than on complete, structured evidence. Version 0.5.0 improves that evidence at several layers.

ntnt lint, ntnt validate, and ntnt check recover from parse errors and report up to five errors per file in one pass. The normal parser used by run, imports, and the REPL still stops at the first syntax error; recovery is deliberately limited to diagnostic commands, where a partial syntax tree is useful and execution would not be. Semantic checks are suppressed on that partial tree so one missing delimiter does not generate a page of invented type errors.

Contract violations now point at the failing requires or ensures clause, show the runtime values involved, and include the call site. An error can tell you that b != 0 failed, show where: b = 0, and identify the call that supplied it. That is enough information to repair the caller without reproducing the failure in a separate debugging session.

Unknown method calls now get spelling suggestions and a bridge back to NTNT's free-function model. If an agent writes a method-like call that does not exist, the diagnostic can suggest the nearest name and say try len(s) rather than stopping at “unknown method.” Three new lint rules cover particularly misleading shapes:

  • unknown_method catches unresolved method calls;
  • block_binding catches block-shaped code that looks like a map but is not one;
  • static_contract_violation flags literal-argument calls that provably violate a function's requires clause before the program runs.

The JSON interfaces changed with the richer diagnostics. Each recovered parse error contributes to summary.errors, parse errors include a column, and ntnt parse --json emits contract clauses as objects containing both the expression and its source line. Consumers should adopt those shapes rather than assuming 0.4.12's output.

Intent files get a linter of their own

ntnt intent lint checks an intent file without starting the application. It reports unresolved glossary terms, near-miss names, definition cycles, vacuous scenarios, and orphan entries, and it supports --json for CI and agent tooling.

The release also fixes a more serious intent bug found during DD-063: when an intent file declared invariants before features, parser state could lose feature IDs and scenario outcomes. A scenario could appear to pass while verifying nothing. Version 0.5.0 preserves that state correctly and includes lint coverage for vacuous cases. This is the kind of bug that justifies treating verification infrastructure as production code. A false negative is annoying. False assurance is worse.

A current verification loop now looks like this:

ntnt lint .
ntnt lint --strict .
ntnt validate .
ntnt intent lint server.intent
ntnt intent check server.tnt
ntnt intent coverage server.tnt

That is more demanding than “the server started.” It is also much closer to an answer an agent can trust.

Declarative input validation joins the standard library

Web applications repeatedly perform the same work around forms and JSON payloads: check required fields, normalize strings, coerce numbers and booleans, reject malformed email addresses, apply bounds, and return useful field-level errors. Version 0.5.0 adds std/validate so each application does not invent a slightly different version.

import { schema, validate, email, min_length, optional } from "std/validate"
import { trim } from "std/string"

let signup = schema(map {
    "email": [trim, email],
    "name": [trim, min_length(2)],
    "age": [optional, int]
})

let result = validate(signup, payload)

validate returns Ok(cleaned) with coerced values and unknown keys removed, or Err(errors) with field-to-message entries. Fields are required by default. optional permits absence, while default(value) supplies a value and then runs the remaining rules against it.

Rules compose existing validators and conversion functions rather than creating a second type-conversion system. Built-in choices include email and URL checks, numeric and length bounds, one_of, regular-expression matching, defaults, and custom fn(value) predicates. The result is intentionally ordinary NTNT data: no hidden request context, no exception path, and no framework-specific validator object leaking into the rest of the application.

Email becomes a built-in capability

std/email adds SMTP sending without adding a package manager or an external dependency tree. Configure a transport once, then send one message or a batch:

import { configure_email, send_email } from "std/email"

configure_email(map {
    "transport": get_env("NTNT_EMAIL_MODE") ?? "smtp",
    "host": get_env("SMTP_HOST") ?? "",
    "from": get_env("SMTP_FROM") ?? "[email protected]",
    "username": get_env("SMTP_USERNAME") ?? "",
    "password": get_env("SMTP_PASSWORD") ?? ""
})

let sent = send_email(map {
    "to": "[email protected]",
    "subject": "Welcome",
    "text": "Your account is ready."
})

Sending returns Ok with a message ID and transport name or Err(message). Invalid addresses and SMTP failures are values, not process crashes. send_email_batch reuses one pooled connection and returns a result for each message, so one bad address does not cancel the rest.

Development and CI can use the log transport, either in configuration or with NTNT_EMAIL_MODE=log. It executes the same application path, writes the message to stderr, and reports success without contacting an SMTP server. TLS behavior follows the port: 465 uses implicit TLS, while other secure SMTP ports use STARTTLS. The implementation uses rustls rather than requiring OpenSSL on the deployment host.

Multi-worker servers are less fragile

The HTTP runtime's worker model received the unglamorous work that makes concurrency usable outside a benchmark. ntnt run --workers N now joins the existing NTNT_WORKERS configuration path, and crashed workers are supervised and restarted with backoff instead of quietly reducing server capacity.

Module-level scheduled loops still run exactly once on the primary interpreter. Worker interpreters skip them, as they skip listen(), so increasing HTTP concurrency does not duplicate background schedules.

The database benchmark behind this work moved the TechEmpower-style single-query route from about 8,300 requests per second with one worker to about 39,000 with eight workers. That result does not mean every application becomes 4.7 times faster; blocking I/O, query cost, connection limits, and route behavior still decide the useful worker count. It does show that the runtime can turn available cores into real throughput while preserving one-owner behavior for schedules.

Small standard-library additions remove common footguns

Two smaller helpers round out the release.

paginate(total, page, per_page) in std/collections returns clamped pagination metadata: offset, limit, page count, and previous/next flags. It centralizes the boundary cases that otherwise get rewritten on every list page.

from_now(timestamp) and time_ago(timestamp) in std/time produce relative labels such as “3 hours ago,” “in 2 days,” and “just now.” They are modest functions, which is exactly why they belong in a batteries-included standard library instead of being copied between applications.

The release also tightens arity checks for is_some, is_none, is_ok, is_err, and unwrap_or. Calls with the wrong number of arguments now fail during lint rather than waiting for runtime or producing a confusing result.

The documentation now labels the future as the future

The original NTNT whitepaper is intentionally ambitious, but ambition becomes a problem when a reader or an agent cannot distinguish a design direction from shipped behavior. Version 0.5.0 adds a prominent implementation-status section separating what exists today from what remains research or roadmap work.

Intent verification, runtime contracts, gradual typing, agent-readable diagnostics, secure web primitives, jobs, and message-passing concurrency are shipped. Typed effects, session-typed protocols, structured AST editing, semantic-version enforcement, first-class PR and CI objects, and declarative human-approval constructs are not. The aspirational sections remain in the paper, but they are now labeled honestly.

The IAL reference received the same treatment, removing references to invariant primitives that were described but never implemented. The agent guide now covers indexing semantics, validation, email, worker parallelism, and the intent lint workflow.

Upgrading from 0.4.12

Most application code does not need syntax changes. The main migration pressure comes from verification becoming honest about behavior that warn mode tolerated.

Run the full checks under 0.5.0 and look specifically for:

  1. out-of-bounds array or string access that should use ??, ?, or otherwise;
  2. implicit conversions exposed by strict verification;
  3. wrong-arity Option and Result helper calls;
  4. intent files with unresolved terms, cycles, orphaned glossary entries, or scenarios that do not exercise anything;
  5. tooling that parses lint or contract JSON using the old shapes.

If a project needs a staged migration, set NTNT_TYPE_MODE=warn explicitly in the affected verification job and remove it as the surfaced issues are fixed. Do not leave the mode implicit and assume the old behavior survived.

A stricter kind of useful

The performance work in 0.4.12 made ordinary NTNT application code cheaper to run. Version 0.5.0 makes ordinary NTNT mistakes cheaper to find.

That matters disproportionately in agent-written software. An agent does not need a diagnostic to be sympathetic. It needs the file, line, column, error code, relevant values, and a verification command whose success means what it says. Multi-error parsing reduces turns. Contract frames reduce reproduction work. Intent lint catches empty assurance before runtime. Strict verification refuses to call a warning a pass.

NTNT 0.5.0 is still a young language, and there are larger runtime and tooling projects ahead. This release does something more immediate: it makes the existing loop tighter, louder, and harder to misunderstand. For a language designed around agents doing the implementation work, that is not polish around the edges. It is the product.

Read the full v0.5.0 release notes on GitHub, or start with the NTNT learning guide.

← NTNT 0.4.12: Cutting the Repeated Work Out of the Hot Path