sprig and prose docs classic docs
Reference› ADRs› ADR011 ExpectsBlock

ADR011 ExpectsBlock

Title: Expects block

Context

A dimension declares the vocabulary its members may speak. error owns hasErrorCode and isCodeFor, so any error is free to write that edge. Nothing on the dimension says an error is expected to write it.

Because of that, prose cannot tell an unfinished concept from a finished one. adr Literals carried no hasStatus while the other twelve records in that file had one. syntax Describe sat without a hasMaturity while the other eight syntax concepts carried one, and it took a manual read of the file to notice. Neither is distinguishable, as a declaration, from a concept that is complete and simply has nothing further to say. errorStatus ActiveError needs a name and nothing else; an error with no code is missing something.

The field guide guesses at this. LooseEnds.svelte reports what is undescribed, what matches a placeholder pattern, what has no edges, parent or children, and which names look like the same thing written twice. Those are four universal heuristics applied to every kind at once, and the component's own comment explains why they are behind a button: "showing a newcomer everything unfinished as part of their introduction misrepresents the place." A heuristic cannot know which absences the author cares about.

ADR 009 met the same wall from the receipts side and chose silence, because a missing mark is "either noise or a spoiler" when prose cannot tell a name that ought to exist in code from one that never should. That record named its own revisit condition: "if prose later grows a way to mark a name as 'this ought to exist in code', the silence rule should be revisited."

The question this record answers is whether a dimension can state the shape it expects of its members, without prose becoming a language that refuses work.

Decision

Add an expects block to the dimension body, written in the syntax a member would use.

dimension error {
  relationship hasErrorCode and isCodeFor { }

  expects {
    describe { }
    relationships {
      hasErrorCode { errorCode }
    }
  }
}

It is a hint, not a schema. A schema states what is permitted and refuses what is not. This states what the author expects to find. An unmet expectation is recorded and reported; it never fails a compile, and there is no SP code for it.

No new vocabulary. describe and relationships inside expects are the same blocks a member writes. The keyword budget grows by one word.

Presence only. No cardinality, no ordering, no counts, no value constraints. If those are ever wanted they are a different block, not a wider expects.

Manifest. A dimension carries its expectations as stated facts, and the unmet set per member alongside them. Emitting that set is the compiler's whole contribution. It forms no opinion about whether an unmet expectation matters.

UI. The field guide reads expectations where it already gathers loose ends. That turns one of the four readings from a universal guess into an authored one, per kind.

Enforcement is a consumer's decision. A team that wants a gate may fail its own build on an unmet expectation. The language will not.

Consequences

What this earns. Absence becomes legible. ADR 009 could not score anything because nobody had said which names ought to exist. An expectation is the author saying exactly that, so the gap belongs to them and it carries meaning. The compiler still never decides on its own that a concept is incomplete, which is the property that kept 009 honest.

Authoring. A dimension may say nothing, as today. A dimension that does declare expectations opts its whole membership into a reading. Because expects sits per dimension rather than on the universe, error can demand a code while errorStatus demands nothing, which is the distinction no global rule can draw.

Cost. Every dimension with an expects block grows a permanent report surface, and a report nobody reads does nothing. The syntax is also read in a different mood inside expects than outside it: describe { } on a concept is an empty description, and inside expects it is a demand for a non-empty one. someEdge { } is an edge with no target outside, and an edge to anything inside. That inversion is the price of adding no vocabulary, and it is only safe if it is total — so the body allowlist, and the meaning of every block inside it, has to be settled in one pass rather than grown.

What this does not do. It does not validate what may nest inside an instance, so it does not satisfy condition (1) of ADR 004. That condition is about permitted containment; this is about expected content. Nested dimensions remain deferred on their own terms. It also does not constrain values, does not apply to concepts or to the universe, and does not give the compiler a way to refuse an incomplete universe.

What is not settled. Two questions are open and should not be answered by the implementation.

  1. A dimension name inside expects. The useful reading of hasErrorCode { errorCode } is "each member should point this edge at some member of errorCode." That is quantification over members, which ADR 006 explicitly refused as the meaning of a bare dimension name, reserving it for "a different form." Inside expects a dimension name would be a set to draw from rather than a node to point at — the first position in prose where that is true. It may well be the right reading, but it has to be stated on purpose, and it has to say whether ADR 006's refusal still holds everywhere else. If the ADR 006 reading is also wanted here — the member must point at the kind itself, as one fact — there is currently no way to write it.
  2. Knowingly unmet. There is no way to record an expectation as deliberately not met. The date-of-death case argues the need is real, since the honest answer there was neither silence nor invention but an explicit unknown. Deferred on purpose: a list of unmet expectations must first acquire permanent, legitimate residents in a real universe. If it does, that is the evidence for a form that records "expected, knowingly absent." If it does not, a keyword was saved.

What to watch. Requiring a describe invites filler. "Accepted ADRs are ADRs that are accepted" is prose's version of a phone number typed as 555-555-5555, and filler that looks like content is worse than the italic undescribed state the field guide already renders without shame. Per-dimension opt-in makes this manageable rather than solved.

The word has been used in this project before, for something else. The archived scenes compiler had an expects block holding project blocks, declaring what a scene was expected to project (parseExpectsBlock in archive/scenes/src/compiler/compile_scene.js).

That is not a collision, and the reason is the distinction this record turns on. The scene version was a schema: a machine contract, checked against a running thing. This one states author intent, and checks nothing. They are not two meanings of one word competing for a grammar; they are the two sides of the line 013 drew, and the schema side was removed by that decision. The resemblance is worth noting only because a reader who finds the archive first will meet the word in the sense that no longer exists.

Alternatives considered

Enforce the shape. An unmet expectation becomes an SP code and the universe does not compile. Rejected, and this is the decision the record exists for. Enforcement corrupts the data it is meant to protect. The case that settled it came from clinical software: recording a patient as deceased was going to require a date of death, which leaves a user who knows the patient died and does not know the date with two options, both worse than an empty field — withhold a true fact, or invent a false one. A required field does not produce complete data, it produces data that cannot be trusted. Prose is not exempt because the author writes both sides; the fastest way past a compile error is filler, and filler in a describe block is the failure. Note also that every existing SP code answers "what you wrote is broken," while this class would answer "what you have not written yet." No universe is ever finished, so that class cannot be fatal.

Name it requires. Rejected. The word promises enforcement, and a reader who meets requires and finds it advisory learns to distrust the language's own vocabulary. expects already carries the possibility that the thing is not there.

A global completeness rule. Every concept must have a describe, declared once on the universe. Rejected. It cannot distinguish error from errorStatus, which is the same blunt instrument ADR 009 rejected as a badge, and it would produce filler at scale.

New vocabulary for shape. A dedicated grammar for expectations, with its own words for optional, required, and repeated. Rejected. syntaxMaturity exists to protect the keyword count, and the shape is already expressible in the syntax a member uses.

Cardinality, ordering and value constraints now. Rejected for this round. Presence is the part that makes absence meaningful, and it is the part with no design questions left. A later block can carry the rest without widening this one.

Leave loose ends heuristic. Keep the four universal readings and add nothing. Rejected. A heuristic cannot tell which absence matters, which is precisely the problem ADR 009 could not solve. The heuristics stay useful for universes that declare no expectations; they stop being the only reading available.

Evidence

The clinical case is the load-bearing one, and it comes from outside this project: a product requirement to make date of death mandatory when marking a patient deceased, pushed back on because it forces suppression or fabrication. The pattern is established enough that HL7 and FHIR both carry an explicit reason-a-value-is-absent concept, so that "unknown," "not applicable" and "not asked" are recordable facts rather than one empty cell. Absence has flavours, and a required field collapses them.

From this repository, two absences that a reader had to find by hand: adr Literals carried no hasStatus among thirteen records, and syntax Describe carried no hasMaturity among nine syntax concepts, both until a manual review. Neither was caught by the four LooseEnds heuristics, because both concepts have prose and have edges, and both are exactly what an expects reading would have surfaced.

ADR 009's fixture is the prior instance of the same problem: twenty-four receipts against twenty-five errors, where the one without a receipt is retired on purpose. That record chose silence because prose had no way to say which names ought to exist. This one is that way.

Expects as an ordinary relationship

This arrived after the proposal, from the Schitt's Creek universe, once 025 had let a relationship be connected, on the pair and on each side. 025 refused a keyword for direction because it "puts an ontology back into the grammar," and left the fact to an ordinary edge in the author's own vocabulary. 029 said much the same of relates. The same move is available here: an author declares expects and attaches it to the relationship it is about.

relationship expects and isExpectedBy {
  describe {
    This relationship is meant to be attached to other relationships. When a
    given relationship expects to have a certain target, you can define that
    target via expects.
  }
}

dimension event {
  relationship involves and isInvolvedWith {
    describe {
      Events generally involve one or more characters, things, or locations.
    }

    involves {
      relationships {
        expects {
          character
          thing
          location
        }
      }
    }
  }
}

The describe { } this record expects has no name an edge could point at, so a stand-in concept takes its place, and the dimension states it in its own relationships block:

dimension syntax {
  relationship mustHaveA and mustBeDefinedFor {
    describe {
      It may be desirable for an author to define whether or not a particular
      dimension should have specific blocks defined, and that's how this can be
      achieved.
    }
  }
}

syntax DescribeBlock { }

dimension event {
  relationships {
    mustHaveA { DescribeBlock }
  }
}

Compiled against the parser at 0.14.0, on a copy of that universe:

input result
expects { character thing location } in the involves side block 0 errors. Three expects entries on the side, each to a dimension id such as SchittsCreek.character. Nothing is mirrored onto the dimensions.
character { describe { … } } inside that expects 0 errors. A note on that entry, like any edge block.
mustHaveA { DescribeBlock } in dimension event 0 errors. One entry in event's edges, mirrored as mustBeDefinedFor on DescribeBlock.
mustHaveA { involves } in dimension event, bare or as SchittsCreek.event.involves [SP007] Target "involves" was not found.
mustBeDefinedFor { event } in the involves side block, with the pair declared in dimension syntax [SP015] Relationship "mustBeDefinedFor" was not found in reachable scopes.
the same, with the pair declared at file scope 0 errors. One entry on the side, to SchittsCreek.event. Nothing is mirrored onto event.
the same on the happensIn side, which episode declares 0 errors, written inside dimension episode. event gains nothing.

What the sketches remove from this record:

  • The keyword. The budget stays where it is. 025's further worry, that every property anyone wanted next would cost another keyword, applies here too: this record already says cardinality and ordering, if wanted, are "a different block, not a wider expects."
  • The change of mood. Every block in the sketches means what it means everywhere else, so there is no inversion to make total. The edge block that 029 said expects would have to account for needs no ruling either; on an expects target it is a note like any other.
  • The first open question, as a question for the grammar. character compiles to the kind as one node, the reading 006 gives a bare dimension name. Reading "some member of character" from that edge becomes a consumer's convention, published as 026 asks. The question survives, but prose no longer has to answer it.
  • The naming argument. requires was refused because a reader who finds it advisory learns to distrust the language's own vocabulary. The sketch can say mustHaveA because the word is the author's. Whatever it promises, the language did not promise it.

What they cost:

  • The unmet set leaves the compiler. The manifest holds the edges and nothing about what they mean, so the set this record has the compiler emit becomes each consumer's work, and only for words the consumer publishes that it honours. That is the cost 025 accepted: "a documented convention rather than a keyword." The describe case asks a consumer to honour two of the author's names, the word and the stand-in.
  • Edge presence can only be said from the relationship's end. The absences this record was written for, an error with no code, adr Literals with no hasStatus and syntax Describe with no hasMaturity, are each an edge missing from a member. As its describe defines it, the first sketch states what an edge should target when it is written, and the second states that a block is present. Neither says that every member carries an edge. From the dimension that is SP007, because a relationship word is not a target, which is the refusal 025 made on namespace grounds. From the side it compiles, but only where the side can reach the word, and the fact stays on the relationship. A word declared on another kind puts the expectation inside that kind's declaration, so "every event happens in an episode" is written in episode, and a reader looking at event has to search the universe to learn what is expected of it. That is the scattering 025 refused for facts about a relationship, run the other way.
  • Knowingly unmet is untouched. Neither sketch records an expectation as deliberately absent.

So this does not decide the record. It changes what the record has to show: not that an expectation can be stated, because an ordinary relationship now states one, but that edge presence, said on the member's own kind, is worth a keyword. It is the one expectation here that is about a kind and cannot be written on it, and it is the case this record's own evidence turned on.