Wit

Data: records, collections & access

Wit declares structured data inline with #name: value, where the value is a record { … }, a collection [ … ], or a scalar. Declare it once; a self-organising document reads from it and renders it many ways. Reach into it from prose with @name.field.

#world: { location - Bag End, time - night, storm - true }

For the underlying value system — the six DataValue kinds, JSON mapping, conditions, and coercion — see Data model.

Records

A record is a set of key/value fields. The separator is a spaced hyphen - or a colon — both produce the same field:

#point: { x - 3, y - 7 }
#point: { x: 3, y: 7 }

A nested value omits the separator entirely — the brace or bracket is the signal: { history { years - 31 } } and { tags [ a, b ] } (nested-record).

Fields are separated by a comma or a newline, never by bare whitespace, so inline and block records are identical:

#report: {
  title: Q3 Review
  quarter: 2024
  final: true
}

A trailing comma is tolerated (trailing-comma) and an empty { } is legal (empty-record). Multi-word keys are stored verbatim — years at post (multi-word-key) — and multi-word values are trimmed (multi-word-value).

The comma-in-value trap

An unquoted comma splits fields, because the parser reads it as a field boundary:

#cite: { author - Boud, D., year - 2024 }   ~ E_MALFORMED_RECORD

That is E_MALFORMED_RECORD — "record field missing key". Quote the value:

#cite: { author: "Boud, D.", year: 2024 }

A quoted string makes its bytes literal (commas, newlines become content) and forces a string type; only " and \ are recognised inside, and an unterminated quote is E_UNTERMINATED_STRING. A multi-line record value uses the key: / indented-block shape, terminated by a top-level comma or the closing brace (block-record-multi-line-value and its -with-close / -with-comma-terminator siblings).

Collections

A collection is an ordered list in [ … ]. Items are separated by a comma or newline — not whitespace. That distinction trips everyone once:

#a: [ x y z ]     ~ ONE item, the string "x y z"
#b: [ x, y, z ]   ~ THREE items

Multi-word items are kept whole (multi-word-items); per-item typing is mixed (mixed-types); collections may hold records inline (inline-records) or in blocks (multi-line-records, multi-line-values) and may nest (nested-collection); a trailing comma is tolerated (trailing-comma) and an empty [ ] is legal (empty-collection).

Scalars are typed eagerly

Every bareword value is classified at parse time into one of four types, and quoting forces a string. The type is not cosmetic — it drives conditionals, truthiness, iteration, and how {{…}} coerces a value. The rules, in order:

  • true or false → boolean.
  • null → null.
  • Matches -?(?:[0-9]+|[0-9]+.[0-9]+) → number.
  • Anything else → string.

The number pattern is strict: an optional leading minus, then digits, optionally a single dot and more digits. There is noleading or trailing bare dot, no exponent, and nounary plus. So these all stay strings: .5, 5., 1e3, +1, and "2024" (quoted).

#report: { title - Q3 Review, quarter - 2024, final - true }
title

is a string, quarter a number, final a boolean. The same happens per item in a collection:

#mixed: [ 1, two, true ]

Number, string, boolean respectively.

Classification is not limited to data definitions. Every non-flag parameter value — through pipes, parens, record-arg, form-fill, or colon-scatter — is run through the same probe, so a value passed by any form can arrive as a typed number, record, or collection and be a valid (each) target, @x.field target, or (if) operand.

Conditionals compare by stringifying both sides, so a quoted "2024"and a bare 2024 do compare equal — both stringify to 2024. But "2024" is not a number: the distinction matters wherever a real numeric type is expected, not for equality. See Conditionals for the comparison model.

Access: fields and indices

Data access reaches into a record or collection from prose: @name.field, @collection.0.field, and deeper chains. It is the verified way to put data into a sentence — not {{…}}, which does not interpolate in prose. Follow the handle with a dot and a field name; the chain reads segments until it hits a byte that is not part of one — a sentence-final period ends it cleanly:

source
#keeper: { name - Ada }

The keeper was @keeper.name.
result

The keeper was Ada.

Collections index by zero-based number:

source
#findings: [ { claim - A, strength - high }, { claim - B, strength - low } ]

First: @findings.0.claim. Second strength: @findings.1.strength.
result

First: A. Second strength: low.

Chains go as deep as the data: @keeper.history.years (access-into-nested-record) and @x.y.z.w (deep-chain).

Fuzzy key matching

Keys match by canonical form: lowercase the key, then keep only [a-z0-9] — spaces, hyphens, and underscores drop out, and camelCase collapses. So a field stored as years at post is reachable by any space-free spelling:

source
#keeper: { years at post - 31 }

camel @keeper.yearsAtPost, snake @keeper.years_at_post
result

camel 31, snake 31

Both resolve to 31. If two different keys collapse to the same canonical form, the access is E_AMBIGUOUS_RECORD_KEY.

A space ends the segment

A space terminates the access segment.

So a multi-word key written with spaces does not resolve:

#keeper: { years at post - 31 }

He served @keeper.years at post here.

Only @keeper.years is read — it is unresolved, and at post trails as prose. Multi-word keys are reachable only through the space-free camel or snake forms above.

Missing keys degrade, not fail

A missing field is not a parse error — it is resolved at expand time and rendered as an unresolved marker (<span class="wit-unresolved">), so a typo shows up in the output rather than breaking the build.

The value-block form

A complex value that spans lines or contains nodes is wrapped as a value-block def: #name:!!. This is why some data definitions end in !! — the block form lets the value hold markup a single line could not.

Common mistakes

  • Leaving an unquoted comma in a value (it splits fields).
  • Expecting [ a b c ] to be three items — it is one.
  • Expecting .5, 1e3, or +1 to be numbers — the strict pattern leaves them strings.
  • Assuming "2024" is a number — it is a string that happens to compare equal to 2024.
  • Typing spaces into an access, like @keeper.years at post — use the camel or snake form.
  • Expecting a missing field to fail the build — it degrades gracefully at expand time.
  • Reaching for {{…}} to read data in prose — use @field.

See also

Data model

· Conditionals · Iteration · Record-arg (same grammar) · Interpolation & captures · Escapes & quoting