Surface — Changelog
Per-version history, folded back from each agent-evaluation round.
v0.10.1 (current)
Spec patch closing the two R8 P0 findings. No new features; the v0.10 design surface is unchanged.
-
§6.4.1 — channel-agnostic
[*]realises form (closes P0-V10-1, R8 unanimous). Substrates may writesurface.<A>(...)[*][<label>]to cover every channel branch of a multi-valueauth_channelaction with one clause when the substrate does not differentiate between channels. Explicit per-channel clauses coexist and take precedence. The §6.1.2 branch coverage rule is updated to recognise[*]as covering every channel branch.Resolves the R8 finding that Opus's CloudFront grew +70 LoC purely from
get's 3 channels × 2 body labels = 6 near-identical realises. With[*], the same coverage is two clauses (surface.get(d, p)[*][hit]and[*][miss]). -
§6.4.6.1 — slot resolution precedence (closes P0-V10-2, R8 Opus). The order is now normative:
internal_action preset < defaults < per-action slot line. Reading: presets are a fallback, defaults express project intent, explicit lines win. The docs projection renders the effective value plus a provenance tag.
v0.10
A double-duty round: address the R7 unanimous P0/P1 backlog and
close the longest-standing structural friction (derived surface
state, raised in R5, R6, R7).
Highlights:
derived from <expr>surface state (§6.6). A surface state field can be declared as a projection of substrate state. Surface bodies cannot assign to it; the substrate'smapsblock satisfies the projection. Eliminates the "mirror substrate state in the surface and reassert it on every action" anti-pattern that has appeared in every CloudFront round since v0.5. Closes R5-audit/R6/R7 carry-forward.auth_channelis a set (§6.4.1).auth_channel: { anonymous, signed_request, bearer_token }covers multi-channel endpoints in one slot; bare value is sugar for a one-element set. Closes R7 unanimous P0-V9-1.freshnessslot (§6.4.1). Seventh mandatory slot. Symbolic epochs, not real clocks. Closed enum:strong | bounded(epochs=n) | eventual | stale_while_revalidate(epochs=n) | not_applicable | waived. Pairs with new obligation rule R-FRESHNESS-CHANNEL.defaults { … }block (§6.4.5) andinternal_actionmodifier (§6.4.6). Two ceremony-reduction constructs that keep slot mandatoriness but cut ~70% of the per-action line count on slot-heavy specs. Closes R7 unanimous P1-V9-1/8.- State-field retention annotations (§6.5). State fields may
carry
retention: <cls>, including aprivatemodifier. Makes the §6.4.1 secret-flow check realisable. Closes R7 P0-V9-5. - Obligation rule catalog expanded from 6 to ~12 rules (§15.4). New rules: R-AVAIL-CONSISTENCY, R-AVAIL-CHANNEL-CLASS, R-INFO-FLOW, R-PII-ANON, R-DERIVED-WRITE, R-FRESHNESS-CHANNEL. Each carries an explicit severity. Closes R7 unanimous P1-V9-2.
Spec patches (R7 P0/P1):
- §6.4 — slots apply to surface actions only; error codes renamed to
E_SURFACE_SLOT_*(R7 P0-V9-3). - §15.3 —
acknowledged { … }syntax pinned: per-entrybecause:, duplicate-acknowledge agreement rule, single-action obligation form with worked example (R7 P0-V9-2). - §15.4 — R-AVAIL-CHANNEL clarified: derives a substrate dependency, not a surface-slot value. Separated from R-AVAIL-CONSISTENCY which checks the closure vs. the declared slot (R7 P0-V9-4).
- §5.3 — rule 3 worked example for globally-keyed non-actor state in
observable for u: <Actor>bodies (R7 P1-V9-5); auto-binding extends to substrate-siderealizes when …(R7 P1-V9-6). - §15.3 —
E_ACK_ORPHANdowngraded toW_ACK_NO_RULE(warning, not error). Authors may proactively acknowledge intuitions the catalog hasn't codified (R7 P1-V9-9).
Examples updated:
examples/twitter/:postsmigrated toderived from aggregate UserAccount[u].my_posts using union_set. Usesdefaults { … }to trim slot ceremony on the 5 actions.examples/url-shortener/:defaults { … }block; multi-valueauth_channelonvisit;freshnessslots throughout.
R8 evaluation pending — GPT-5.5 and Opus 4.7 will each author a v0.10 CloudFront spec after a staff-level PL designer review.
PL designer review fold (pre-R8): A sync staff-level PL review
(see docs/reviews/PL_REVIEW_V010.md) was run between the v0.10 draft and R8.
Three of its "must address" findings were folded in before R8:
derived from <substrate-expr>was the PL review's Cut-One pick (least defensible v0.10 decision: violates the surface/substrate abstraction boundary). v0.10 now usesstate { f: T derived [shape: …] }; the projection lives in each substrate'smapsblock. Both the spec (§6.6) and the Twitter example were updated accordingly.- Symbolic freshness epochs had no semantics. v0.10 now requires
substrates to declare named epochs via
epoch <name> { advances_on <action-set> ; covers <field-set> }(new §7.2.4). Freshness slots reference an epoch name; undeclared epochs error. - The obligation pass had no fact schema. New §15.1.5 normatively enumerates the base + derived predicates and states the complexity claim (polynomial in source size; transitive closure for availability dependencies). Monotonicity is restated carefully (derivation is monotone; unhandled-obligation report is not).
Three findings deliberately parked (see §14.1): a formal label calculus for retention/info-flow, slots-as-effect-rows refactor, discharge vs acknowledgement separation. v0.10 commits to explicit-flow only for retention checks; implicit flows are not a violation.
v0.9
The "boundary completeness" round. Three observations drove it:
- v0.7 closed every refinement-related gap from R1–R6, but Surface still couldn't say "you didn't think about idempotency" or "you didn't think about retention." It was a refinement checker, not a boundary-completeness compiler.
- Earlier drafts framed Surface as "a language that compiles to TLA+." That understated the frontend — most of Surface's value should live in static, fast, local checks before TLA+ ever runs.
- P1‑7 (actor-relative surface state) was the last remaining
structural gap. Twitter's
posts/follows/protectedare genuinely not globally observable; the v0.7 spec had no clean home for them.
The four moves:
-
coverage.md— Coverage Manifest. A new normative document enumerating every dimension Surface covers, which construct addresses each, since-version, and a versioned list of what Surface deliberately does not cover. v0.9 is the first version where "what's in scope / what's out" is a first-class artifact rather than scattered across the spec. -
Mandatory action coverage slots (§6.4). Every
actionmust carry six slots —idempotency,auth_channel,retention,rate_limit,observability,availability— each a closed enum with awaived: "<reason>"escape. The compiler errors on missing slots. Substrates'authentication { … }mapping is cross-checked againstauth_channel; emit/log of aretention: secretvalue is a static error. The slot pass runs assurface check --slots, separate from refinement; it is a precondition for codegen. -
observable for <Actor>(§5.3) — closes P1‑7. Actor-relative view projections:observable for u: User visible_posts(u) : Set[...] = …. The compiler binds the actor variable to the calling action'sbyparameter automatically; cross-actor leaks are static errors (R-ACTOR-VIEW-LEAK). Twitter's globalposts/follows/protectedstate migrates to per-user views inexamples/twitter/. -
§15 — Static obligations (sketch). The framework for a consequence-inference pass: derivation rules over declared facts produce obligations the user must
acknowledged { … }or discharge. v0.9 ships the framework, a closed enum of obligation kinds, and six representative rules (R-AVAIL-READ, R-AVAIL-CHANNEL, R-TRUST-PARAM-AUTH, R-WRITE-CONFLICT, R-REPLAY-AMP, R-RETENTION-FLOW, R-ACTOR-VIEW-LEAK). The full rule catalog is deliberately deferred to v0.10.
Plus, a positioning change: TLA+/PlusCal is Surface's primary
backend, not its semantics. The slot pass and the obligation pass
live in the frontend and produce Rust-style local errors with a fix
site. They run on every save; the TLA+ backend runs on a schedule.
overview.md principle #2 and §12 of the spec were rewritten to
reflect this.
Spec changes (cross-referenced to the construct/section):
- New §5.3:
observable for <Actor>(P1‑7). - New §6.4: Mandatory coverage slots — six closed-enum slots with
waived:escape, compiler-error rules, docs-projection checklist rendering, v0.7→v0.9 migration tool sketch. - New §15: Static obligations sketch — framework + obligation-kind
enum +
acknowledged { … }block + 6 representative rules. - §12 Tooling: separate
--slotsand--obligationspasses; reframe TLA+ as one backend among Surface's outputs; tooling table added. - §14 Omissions: P1‑7 removed (closed); full §15 rule catalog and user-extensible rules added as deliberate v0.9 exclusions.
Examples updated:
examples/url-shortener/: actions carry the six slots; some realistically waived (rate_limit, availability) given the spec's didactic scope.examples/twitter/: actions carry the six slots;posts,follows,protectedmigrated toobservable for Userviews.examples/url-shortener/docs-sample.md: refreshed to show the per-action boundary checklist rendered from slots (the P1‑10 mitigation).
Closed: P1‑7. Partially mitigated: P1‑10 (slot checklist softens the property-body cliff in the docs projection; spec text remains a wall for nested quantifiers).
R7 evaluation: GPT‑5.5 and Claude Opus 4.7 each independently authored a v0.9 CloudFront spec as a reviewer exercise. See the R7 row in the agent feedback log for the convergent findings folded into v0.10.
v0.7
A consolidation/hygiene round: no headline new constructs, just
clearing the high-priority small-and-medium backlog from TODO.md.
Drove from the R6 example audit + language-designer critique.
Spec changes (each cross-references the resolved TODO id):
- Cuts to reduce surface area (designer review):
system Foo { … }sugar removed (module Foois the only container) — C-1.permission name(args) for u: User when …declaration form removed; use a Booleanobservableand call it fromwhen— C-3.aggregate using allandaggregate using anyaggregators removed (use a comprehension andchooserespectively) — C-4.
Returned<X>semantics pinned (S0-1, C-2): implicit only, with fields(value: T, to: Actor); explicit declarations are illegal.observed byis checked semantically against the event's recipient field by name resolution (to→by→recipient) — S0-10.- Cardinality (
s.size/m.size/|s|) added for sets and maps — S0-11. Map[K -> V]is a finite partial map; total maps via comprehension; no new type — P0-5.- Effect grammar (P0-7, P0-8, S0-2, S0-4):
aggregate ...is now legal in action bodies, not only inmaps/observable/history_predicate.emitis exempt from thefor ... doorder-independence restriction (events appended in iteration order are atomic per surface step).- Bounded
choose x in <set>. <pred>form added. - Optional unwrap:
if let Some(x) := opt then ... else ...and thematch opt { Some(x) -> e1; None -> e2 }value form.
- Branch coverage rule (S0-6): every leaf branch label of a surface action must be realized somewhere across the project.
- Multi-disjunct guards without labels (P1-9): warning recommending labels for any non-trivial guard split.
- Error refinement (P1-6) — new sub-section showing the two blessed patterns: substrate mirrors the raise guard, or substrate leaves the action disabled (stutter).
aggregatetyping tightened (P1-2, P1-3): splitunion_set(set semantics, deduplicating) fromconcat_seq(order_by ...)(deterministic sequence concatenation);else <expr>clause formax/minempty scopes; map-valued example added.- Realization parameter binding scope (S0-7) — new sub-section
pinning down what's in scope inside
realizeswhenguards. cross_visibleis a two-way contract (P0-6): owning substrate declares the invariant; either composed substrate may write.- Cross-substrate channel worked example added (P1-4) including
the
Component[*].receives.MsgNameform forinternal/fairness. by stutterbuilt-in for compose-level realization of no-effect branches (S0-8, C-5). Drops the informalby no_opand the explicit-noop-action workaround.forbiddenvs.safety+fails withclarified in §8 (S0-5): asserting a deterministic failure issafety, notforbidden.- Event-log helpers: bounded forms
last(seq, pred)/first(seq, pred)/count(seq, pred)added; duplicate-event semantics pinned (slicing helpers index by occurrence;firstoccurrence forevents_before,lastforevents_after) — S0-3, S0-9. state_at(e)helper (P1-5): returns a snapshot of surface state at the momentewas emitted; usable inhistory_predicatebodies. The Twitteris_following_at/is_protected_atpredicates switched to this and are now genuinely time-relative.externis top-level only confirmed (S0-12).- §14 omissions refreshed (cross-module liveness deferred to v0.8; everything else unchanged).
Closed bookkeeping (already done; just marked closed): P0-1 through P0-4, P1-1.
Examples updated:
examples/twitter/:using union→using union_set; theIndex.noopworkaround dropped in favour ofby stutterincompose.surf; history predicates rewritten to usestate_at(e); README and v0.7 feature table updated.examples/url-shortener/: minor refresh; banner bumped.- New:
examples/url-shortener/docs-sample.md(P1-8) — hand-rendered Markdown showing whatsurface emit docswill produce. The "non-technical reader can stop at the surface" claim is now falsifiable; the deferred actual generator just needs to match this shape.
TODO.md: every closed item annotated; remaining backlog is mostly P1-7 (L), the P3-* deferred items, and any new findings from the v0.7 verification audit.
v0.6
Driven by the v0.5 two‑agent CloudFront round. Six convergent findings, all in:
aggregate Component[id].field over <Scope> using <Aggregator>— first‑class aggregation across replicated instances insidemaps,observable, andhistory_predicatebodies. Aggregators:exists,forall,any,all,sum,max,min,union. Eliminates thechoose id … exists id …chains that v0.5 specs needed by hand. Both v0.5 agents named this as the #1 missing feature. (§7.2.3)- Cross‑substrate
realizesoverrides incompose— a compose block may add or strengthen arealizesclause whose guard reads the other substrate's state. The owner declares which auxiliary variables are reachable across substrates withcross_visible. Closes the joint‑refinement gap (every‑edge‑has‑acked, quorum reached, distributed commit). (§7.7.3) sends … to <ChannelName>call‑site syntax spelled out for cross‑substrate channels declared incompose. The channel name is in scope inside any component of either composed substrate. (§7.2.1)- Type signatures and predicate semantics for the event‑log helpers
events_before,events_after,first,last,count,between. The predicate form ise is X && e.field == …. (§9.1.1) - Branch labels (
[hit],[miss], …) must be unique within an action body; the orderwhen→raises→ labeledif/elseis now explicit.raisesalways wins overif/else. Pairwise overlapping raise guards are statically rejected. (§6.1.1) - Tuple keys from
cross—A cross BisSet[(A, B)]; tuples are valid map keys; comprehensions can destructure with(a, b). (§3.1)
Examples:
- Added
examples/twitter/exercisingreplicate,aggregate,history_predicate,attacker,e is X. Single substrate.
v0.5
Driven by the v0.4 three‑agent CloudFront round. The unanimous v0.4 finding was that splitting a system into peer substrates (data plane vs. control plane) was painful because every substrate had to map every surface field.
partial substrate … owns { <fields> }— a substrate may declare it owns a subset of the surface's state fields; it only needs to map those. A plainsubstrate(nopartial) is shorthand for "owns everything." (§7.7.1)compose <Name> = <Sub1> + <Sub2> + …— project‑level block that joins partial substrates into a complete realization, declares any channels that cross substrate boundaries, and runs three project‑wide checks: ownership partition, action coverage, channel completeness. (§7.7.2)replicate×channelscoping spelled out:from A to B[*](broadcast),from A[*] to B(fan‑in),from A[i] to B[i](pairwise),from A[*] to B[*](N×M with explicit destination). (§7.2.1)- Labeled
if/elsebranches referenceable fromrealizes.then if cond then [hit] … else [miss] …on the surface; substrates writesurface.get[hit] by Edge[id].serve_hit. (§6.1) param.<name>as anauthentication { … }RHS. Actor identity may come from an action parameter instead of a long‑lived state field. (§7.5)history_predicateblocks for reusable event‑log predicates so multi‑quantifier history claims stay readable. (§9.1)_wildcards in event matches;e is <EventName>event‑type test with field access on the narrowed type. (§5.1.1)
Examples:
examples/directory introduced;examples/url-shortener/is the first worked example.02-examples.mdremoved.
v0.4
Driven by a hands‑on CloudFront evaluation by Sonnet 4.6 and Opus 4.7
under v0.3. Their #1 finding: no if/else in effects.
- Conditional effects
if <guard> then <effect> else <effect>inthenblocks; compiles to two next‑state disjuncts. letandchooseinthenblocks.let x := eintroduces a binding;choose x: T. P(x)is TLA+CHOOSE.observable name(args): T = exprformalized in §5 (previously appeared in02-examples.mdonly).A cross Bas a first‑class set‑product operator.forall/existsexplicitly allowed inwhenandraisesguards (one expression sublanguage everywhere).by EXTERNALinrealizes— a substrate may declare that a surface action is realized externally; satisfies completeness without a refinement obligation.replicate Component[id in IDS] { … }value‑parametric components.- Fairness disambiguation for multi‑realized surface actions.
authentication { … }RHS grammar formalized: component path orsystem.- Channels gain a documented
sends/receivesform.
Modularity:
- A module may be defined across multiple files. Every
.surffile starts withmodule <QualifiedName>; the compiler unions the contributions.surface check ./project/is project‑aware.
v0.3
Driven by two parallel critiques (a language‑design critique and a formal‑methods critique) of v0.2.
Soundness fixes (formal methods):
- Auxiliary variables (
history/prophecy) first‑class insubstrateblocks. Without these, Abadi & Lamport's completeness result is violated and many real implementations cannot be proved correct against a strong surface. - Fairness part of the language:
fairness weak/strong <action>on surface and substrate. - Compositional reasoning restricted to safety in v0.3; cross‑module liveness deferred.
- Default‑deny for substrate steps: every substrate action must
appear in
realizesorinternal. internalactions emit an explicitUNCHANGED Map(surface_vars)obligation.- Surface → TLA+, substrate → PlusCal by default.
attackerscoped to integrity reachability (not confidentiality).authentication { … }mapping required in substrates so attacker checks aren't surface‑theatre.
Language ergonomics:
- Unified error model:
raises { Name when G };failremoved. invariantremoved; one form:property name { always P }.- Typed
eventdeclarations replacevisible_as "string {templates}". - Actor subtyping (
actor B extends A). externfor spec parameters set at check time.- Expression grammar formalized (
+=/-=/:+, comprehensions, sequence ops,union/intersect/diff). - Action return values added to grammar.
- Scenarios require an explicit
kind:(safety/liveness/forbidden).
v0.2
Decisions locked in after the initial v0.1 sketch:
- Braces‑only grammar, whitespace insignificant.
- Refinement mapping syntax:
maps { … }+realizes { … }+internal { … }. moduleas the canonical container;system Foois sugar for a top‑levelmodule Foo.- TLA+ / PlusCal as the only codegen target; SDK and clocks deferred.
v0.1
Initial sketch. Headline idea: every system has a surface (what users
observe) and a substrate (how it's built); a single .surf file holds
both, with mechanical refinement between them. One file produces:
formal model, refinement obligations, Gherkin/pytest skeletons, prose
docs, threat model.