Repository navigation
Writing Specs
Write the program as ordinary TypeScript and leave return arc.todo(); where the compiler should write a body. The declaration is the spec:
| Part | Comes from |
|---|---|
| contract | the signature (generics allowed; this for a method) |
| intent | the JSDoc, or the JSDoc of the interface member it implements |
| id | the module path, class and member (lb.balancer.roundRobin.pick), or @arc id
|
| behaviour |
arc.test, arc.test.hidden and arc.law: TypeScript, type-checked against the real code, run in the sandbox |
| context | the class shell (fields, constructor, other members' signatures), the types it reaches, the shown tests |
| extra settings |
@arc calls|effects|deps|backend|outputFormat tags, arc.configure(target, { … }), or a defineRegion with the same id |
export class LeastConnections implements Balancer {
readonly backends: Backend[];
constructor(backends: Backend[]) {
this.backends = backends.map((b) => ({ ...b }));
}
/** The healthy backend with the fewest connections (ties: first in list order); it then has one more. */
pick(key: string): string | null {
return arc.todo();
}
}
arc.test(LeastConnections, "picks the minimum, then counts it", (LC, expect) => {
const lb = new LC([{ id: "a", healthy: true, connections: 5 }, { id: "b", healthy: true, connections: 1 }]);
expect(["1", "2", "3", "4", "5"].map((k) => lb.pick(k))).toEqual(["b", "b", "b", "b", "a"]);
});Everything about this form (classes, ids and markers, expect, hidden tests, laws, fakes by type, the customization layer, migrate) is on Code First.
Every form below predates code-first and stays fully supported: nothing is deprecated, and the forms mix in one project and one file. They are also the customization layer for code-first regions (a defineRegion whose id is a code-first region's attaches to it).
type F = (a: A, b: B) => R; // the type is the contract, and it's authoritative
export const fooSpec = arc.defineRegion<F>({
id: "module.foo", // [a-z][a-zA-Z0-9]* segments joined by dots, unique per project
symbol: "foo", // optional; default = last id segment
intent: "…" | ["…", "…"], // guidance for the model; never treated as verified
effects: [], // "clock" | "random" | "log"
deps: ["./helpers.ts", "node:crypto"], // the only imports a body may add
examples: [{ name?: "…", args: [...], expected: …, tolerance?: 1e-6, exact?: false }, { args: [...], throws: true | "…" }],
tolerance: 1e-9, // optional: the default numeric tolerance of every example
calls: ["helpers.slug", "ports.fs.read"], // optional: the only imported/port functions the body may call
sketch: (a, b) => { … arc.hole<T>("…") … }, // optional: the body as typed code with holes (see Sketches)
concept: "auth", fixtures: { … }, waive: { … }, // optional: a variant of a concept (see Concepts)
});Four ways to say what a region does, which mix freely:
| Form | Checked? | Use it for |
|---|---|---|
examples (and a concept's laws) |
yes, in the sandbox | behaviour |
sketch with arc.hole<T>()
|
yes: the generated body must be the sketch with only the holes filled (OARC-E099) |
structure you already know |
ui in a *.spec.tsx file |
yes, in a rendered DOM | a component's structure and wiring |
intent (and hole descriptions) |
no | everything else; optional once a sketch or ui exists |
Where they disagree, the checked forms win: examples, then the sketch, then the UI structure, then calls/effects, then prose.
Specs must be static. Only JSON literals are allowed: no identifiers, spreads, calls or template substitutions. The compiler reads them without running your code (OARC-E010 otherwise). Use the array form for long intents.
The signature never comes from the model. The compiler prints it from F, and the model only returns the body.
expected is compared with deep equality, strict about structure and types, with three exceptions for numbers:
| Case | Rule |
|---|---|
| floats |
a and b match when |a − b| ≤ tolerance × max(1, |a|, |b|): relative for large values, absolute near zero. tolerance comes from the example, else the spec, else 1e-9
|
-0 |
equals 0. JSON can't write -0, so fitLine returning [-0, 1, 0] for a horizontal line passes expected: [0, 1, 0]
|
NaN |
equals NaN
|
exact: true on an example compares numbers bit for bit (Object.is: -0 differs from 0, no tolerance; NaN still equals NaN).
JSON has no NaN, Infinity or undefined, so write them as string tokens, in expected and in args: "$NaN", "$Infinity", "$-Infinity", "$undefined". The harness decodes them wherever they appear (fake init values excepted), and the Example type accepts them where F has a number or undefined:
type SegDist = (p: Point, a: Point, b: Point) => number;
examples: [{ args: [[1, 1], [2, 2], [2, 2]], expected: 1.4142135623730951 }, { args: [["$NaN", 0], [0, 0], [1, 0]], expected: "$NaN" }]Failure messages print values faithfully: got [-0,1,NaN]; expected [0,1,0], not got [0,1,null].
Recursive types work in F: type Nested = Array<number | null | undefined | Nested> type-checks in args and expected (before 0.7.1 it was TS2589 on the spec itself). The tokens are accepted down to 8 levels of nesting; deeper values are checked against the plain type.
Write throws instead of expected when the call must fail. throws: true passes when the call throws (or its promise rejects) with anything; a string passes when it appears in the error's message:
type Find = (xs: number[], x: number) => number;
examples: [
{ args: [[1, 3, 5], 5], expected: 2 },
{ args: [[1, 3, 5], 4], throws: "not found" },
{ args: [[], 1], throws: true },
]An example has expected or throws, never both (the type rejects it). fakesAfter still applies after the throw, and a body that is still pending counts as pending, not as a throw. When a body returns instead, the failure says what it returned: got {"returned":0}; expected {"throws":"not found"}; a different message prints as got {"throws":"<message>"}. The context package shows these examples to the model like any other.
Examples are the only behaviour OpenArc checks, and in the evaluation three regions passed every example while disagreeing with the original code on ordinary inputs (coverage 50/80, outputFileName 8/40, resolveChannelId 1 of 5 URL forms). Two tools help:
-
OARC-W044warns, when a function region is compiled, if it has fewer than 3 examples (detailexamples:<n>) or if no example has an edge-case argument:"",[],{},0, a negative number,null, or a$NaN/$Infinity/$undefinedtoken (detailno-edge-case). It's a cheap syntactic heuristic, so a boundary such as5in a1..5range goes unnoticed. -
openarc check <id> --oracle <module>#<export>compares the compiled region with a reference implementation you already have (the code you are porting). It runs both, in the sandbox, on the examples' argument lists plus seeded variations of them (60 inputs, seed 1, so the run is reproducible), and prints every input where they disagree. Two results agree when they are equal under the rules above, or when both throw. Only arguments made of numbers, strings, booleans, and arrays or objects of those are varied; examples with fakes ornullare skipped. Variations keep shapes: a top-level array changes length only if the examples show different lengths there (so[x, y]stays a pair), and strings are sometimes swapped for another example's string at the same position (enum-like values). The module must be inside the project and self-contained (only it is copied into the sandbox);.jsworks.
$ npx openarc check text.outputFileName --oracle legacy/text.js#outputFileName
#14 args ["João","Mirão",2024,"x"]: got "Joo_Miro_2024_x.txt"; oracle "Joao_Mirao_2024_x.txt"
check text.outputFileName against legacy/text.js#outputFileName: 52/60 inputs agree (seed 1)
Add the inputs it finds as examples, and recompile.
openarc lint checks specs before any compile, without a model: contradictory or duplicate examples, regions nothing verifies, empty intents, thin examples (the W044 rule), and in the strict profile missing boundary values and uncovered return values. It's fast enough for a pre-commit hook or CI. The rules, their profiles and the suppression comment are in CLI Reference; the lint block in Configuration.
| Guaranteed (mechanically checked) | Not guaranteed |
|---|---|
The body type-checks against F
|
Behaviour beyond your examples |
| Only declared imports and effects | Following the intent prose |
| Your examples pass, in the sandbox | Identical output on a fresh regeneration |
| Hand edits are never overwritten | |
| A failed compile writes nothing | |
| A sketch region keeps its sketch (only holes are generated) | |
| A concept variant passes its concept's laws | |
A UI component has the structure, labels and wiring its ui describes |
Hand-write a function's control flow and leave the steps abstract:
export const stepSpec = arc.defineBlock({ id: "job.run.load", intent: "rows = await loadRows(db, req.table)" });
// @openarc:begin job.run
export async function run(db: Db, req: Req): Promise<Result> {
let rows: Row[] = [];
// @openarc:begin job.run.load
arc.pending("job.run.load");
// @openarc:end job.run.load
if (rows.length === 0) return { ok: false };
…
}
// @openarc:end job.runRules for blocks:
-
Say which functions it may call.
calls: ["loadRows"](see below). A block is verified only through its function's examples, socallsis the cheapest way to pin down what it does. -
No escaping control flow: no
return/break/continueout of the block, and nothis,argumentsorsuper. Arrow functions are transparent tothisandarguments, so those are caught inside arrows too. -
Communicate through
letvariables declared above the block. The parent owns all control flow. -
They're verified through the enclosing function's examples. Sibling blocks are jointly re-verified before commit, and some passing example must reach each block: a block that every example returns before is unverified (
OARC-W045), not cached as verified and not committed. Add an example that goes all the way through.
When you know how a function should work, write it in the spec and leave holes only where you want the model. A sketch is real TypeScript, typed by F, read as syntax and never run:
export const passwordSpec = arc.defineRegion<Strategy<PasswordCred>>({
id: "auth.password",
calls: ["ports.users.recordFailure", "ports.clock.now"], // what fills may call besides what the sketch calls
examples: [/* … */],
sketch: async (cred, ports) => {
const email = arc.hole<string>("cred.email trimmed and lower-cased"); // expression hole: one expression of type T
const user = await ports.users.findByEmail(email);
if (user === null) return { ok: false, reason: "invalid" };
const valid = await ports.hasher.verify(cred.password, user.passwordHash);
arc.hole("if not valid, record the failure for user.id"); // statement hole: 0..n statements
return valid ? { ok: true, userId: user.id } : { ok: false, reason: "invalid" };
},
});-
Holes.
arc.hole<T>("…")as an expression is one expression of typeT(written, or inferred:const n: number = arc.hole("…")). As a statement it is zero or more statements, with the block rules: noreturn,breakorcontinueout of it, nothis, no declaration the sketch uses later (OARC-E035). A function-valued hole (arc.hole<(s: string) => string>("…")) is a local helper. Descriptions are string literals; two statement holes in a row are ambiguous (OARC-E010). -
Checked before the model. The sketch type-checks against
Fwith your file: a hole of the wrong type isOARC-E070(detailsketch:TS<code>) before any model call. -
What the model does. It sees the sketch with
«1»,«2»… in place, one line per hole (its type and description), and replies{"fills": {"1": "…", "2": "…"}}. The compiler splices the fills (an expression fill is parenthesised) and commits the sketch with them as the region body. A model that sends a whole body anyway is not rejected outright: containment recovers its fills. WithoutputFormat: "code"there is no schema to carry per-hole fills, so the model is asked for the whole filled sketch as one code block and containment recovers the fills the same way. -
Containment (
OARC-E099). The body must be the sketch with only the holes filled: comments, whitespace and quote style don't matter, anything else does (body line 3 has 'invalid' where the sketch has "unknown"). -
Calls. Fills may call what the sketch itself calls plus
calls(OARC-E098otherwise). Most sketches need nocallsat all. The sketch's own calls are never checked: you wrote them. -
Coverage (
OARC-W045, detailhole:<id>#<n>). Every hole must be reached by a passing example, or the region isn't committed. Add an example that goes through it. -
OARC-E100. An example that fails without reaching any hole is decided by your sketch, so no fill can fix it: the compiler says so after one attempt instead of spending the rest. - Parameter names come from the sketch,
intentbecomes optional, and callees the sketch calls are compiled first.
A block region is a statement hole in code you edit in place; a sketch keeps the structure in the spec and allows expression holes. Both stay.
A concept is a family of regions with a shared intent and laws, the rules every variant must obey. Each variant is an ordinary region that names the concept:
export const authConcept = arc.defineConcept({
id: "auth",
interface: "Strategy", // optional: each variant's F must be Strategy<…> (else OARC-E020 concept-interface)
intent: "Authenticate one credential. An unknown user gets the same answer as a wrong secret.",
laws: [
{ name: "wrong secret is rejected", args: ["$fixture:wrong", "$fixture:ports"], expected: { ok: false, reason: "invalid" } },
],
});
export const apiKeySpec = arc.defineRegion<Strategy<ApiKeyCred>>({
id: "auth.apiKey",
concept: "auth",
intent: "…",
fixtures: { ports: { $fake: "palette.auth", init: { … } }, wrong: { apiKey: "k1.guess" } },
waive: { "some law": "why it can't apply here" },
});- Every law becomes an example of every variant, named
law: <name>, with"$fixture:<name>"replaced by the variant's fixture (a missing one isOARC-E010 fixture-missing:<name>). Laws are as authoritative as your own examples. -
waiveskips a law with a reason;compileprints it and the manifest provenance records it. - Changing a law recompiles every variant. A region that calls a variant sees its signature and the concept's one-line summary, never its body.
To build a library of your own concepts (a worked cache concept with LRU and TTL variants), see Your own palette. The bundled palette built this way (auth, load balancing, p2p, a login page) is deprecated and frozen since 0.9.0.
A UI region's spec goes in a *.spec.tsx file and describes the component's rough structure with a small vocabulary; the region is a React component in the sibling .tsx file:
/** @jsxImportSource open-arc/ui */
import * as arc from "open-arc";
import { ErrorText, Field, Form, Submit } from "open-arc/ui";
export type LoginPageFn = (props: LoginPageProps) => unknown; // an exported type name: the component file imports it
export const loginPageSpec = arc.defineRegion<LoginPageFn>({
id: "ui.loginPage",
symbol: "LoginPage",
ui: (props) => (
<Form title="Sign in" onSubmit={props.onLogin} onSuccess={props.onSuccess}>
<Field name="email" type="email" label="Email" required autoComplete="username" />
<Field name="password" secret label="Password" required autoComplete="current-password" />
<ErrorText />
<Submit busyText="Signing in…">Sign in</Submit>
</Form>
),
examples: [{
name: "a wrong password shows the error",
args: [{ onLogin: { $fake: "action#onLogin", init: { returns: [{ ok: false, error: "Wrong email or password." }] } }, onSuccess: { $fake: "action#onSuccess" } }],
script: [{ fill: { label: "Email", value: "ana@x.io" } }, { fill: { label: "Password", value: "nope" } }, { click: { role: "button", name: "Sign in" } }],
dom: { text: ["Wrong email or password."], enabled: [{ role: "button", name: "Sign in" }] },
fakesAfter: { "action#onLogin": { calls: [[{ email: "ana@x.io", password: "nope" }]] } },
}],
});-
Vocabulary:
Form,Field,ErrorText,Submit(fromopen-arc/ui). tsc checks the attributes; the compiler reads the tree without running it. Spreads,.map, conditionals, computed attributes and other tags areOARC-E010(ui:*): repetition and conditions belong in the intent. This built-in vocabulary is deprecated and frozen since 0.9.0: it keeps working unchanged, and the mechanism (the static reader, the DOM harness,ui.dom) stays supported; project-defined vocabularies come with code-first phase 5. -
Checks, in a DOM in the sandbox: derived structure and accessibility (each Field is an input whose accessible name is its label, with the right name, type, required and autocomplete; an alert for ErrorText; a submit button named by its text), a derived wiring example (fill every field, submit, the prop
Form onSubmitpoints to is called once with{ name: value }, and while it is pending the submit button is disabled and showsbusyText), then your examples:scriptsteps (fillby label,clickby role and name),domassertions (text,notText,enabled,disabled) andfakesAfter. -
action#<name>is a built-in fake: a function that records its calls and returnsinit.returnsin order. -
Setup:
"ui": { "dom": "jsdom" }inopenarc.config.json, andjsdom,react,react-dom(plus@types/reactand"jsx": "react-jsx"in tsconfig) in the project. Without them a UI region isOARC-E092(dom:not-configured,dom:jsdom-missing, …) before any model call.
calls is an optional allowlist, on defineRegion and defineBlock, of the imported or port functions the body may call:
export const unlockSpec = arc.defineBlock({ id: "compile.run.unlock", intent: "await ports.fs.remove('.openarc/lock').", calls: ["ports.fs.remove"] });
export const parseStepSpec = arc.defineBlock({ id: "compile.run.parse", intent: "project = await engine.parse(ports, req, diagnostics);", calls: ["engine.parse"] });Every call in the body whose root is one of the file's imports (or a requested import), or a parameter of type Ports, must match an entry exactly or a prefix.* entry ("engine.*"). Anything else is OARC-E098 call not allowed by spec: <path> (allowed: …), and the model gets another attempt. Calls on locals (diagnostics.push, rows.map), on functions the body defines, and on globals (governed by effects) are never restricted. The prompt says "You may call exactly: …".
Why: in OpenArc's own self-compile, three compile.run blocks were cached as verified while doing the wrong thing: unlock released the lock and then re-ran parse through commit, recover ran a commit, and analyze parsed twice. Their function's only conclusive example returned before reaching them. With calls, all three are rejected before any example runs.
| State | Meaning |
|---|---|
pending |
body is the placeholder; will be generated |
compiler |
body is exactly what OpenArc committed; may be regenerated when stale |
stale (display only) |
a compiler region whose spec (or, when known, fingerprint) no longer matches the manifest: the body was verified against an older spec. graph, inspect and watch show it; if its recompile fails, compile reports OARC-W043 and exits 1. Ownership is still compiler
|
developer |
you edited it, froze it, or wrote it yourself; never written by OpenArc |
conflicted |
spec changed under a developer-owned body, or markers were damaged; resolve explicitly |