Migrating to 0.15
0.15.0 opens a new compatibility key: it reads pins of 0.15.x only. A project pinned to 0.14.x is refused by name until it is upgraded, and listen-fire upgrade moves its pin once every file checks clean.
One idea runs through the release: a type says what the run will meet. A name is never a word the language reads as itself; coalesce answers the kinds its arguments hold; a key, a field or a built-in's answer that may be empty says so; and a collection in no order is never read as if it had one. 0.14 let each of these check clean, and the run failed or answered something arbitrary.
Contents #
- 1. How the pin moves
- 2. A word an expression reads as itself is never a name
- 3. undefined is typed as undefined
- 4. Airtable and Google Sheets cells that can be empty
- 5. A undefined or undefined key that may be absent
- 6. The records a block answers come in the walk's order
- 7. Inside walk brackets a built-in is spelled as outside them
- 8. A built-in that can answer nothing says so
- 9. A whole program
- 10. Diagnostic code index
1. How the pin moves #
listen-fire upgradechecks every movement file as a move up from its pin. It rewrites one change unasked, a walk word to its everyday spelling (section 7), exceptLENGTHof a list. Each other change is a refusal the author fixes by hand, because each fix chooses a value (a name, a fallback, an order).- The pin (
engine:inlf.yaml) moves to0.15.0only when every file then checks clean. Otherwise it stays, and the command lists what is left. - 0.15 changes what a run reads in one place: an Airtable checkbox left empty is
falseand an empty list field is[], where 0.14 read both as absent (section 4).
2. A word an expression reads as itself is never a name #
true, false, null, and, or, not, in, contains, exists, if, then, else and end, in any letter case, are refused as a binding, a parameter, an alias or a function name (MOV_V5_HIDES_BUILTIN). 0.14 checked true = 5 clean, and every later true was the literal.
| 0.14 | 0.15 |
|---|---|
end = Date.addDays(start, 7) | deadline = Date.addDays(start, 7) |
(in) => … | (item) => … |
3. coalesce is typed as a ?? b #
coalesce answers the union of what each argument it reaches holds, as TypeScript types a ?? b. An argument that is always there is the last one reached. 0.14 typed arguments of different kinds as unknown, and every later check said nothing.
| 0.14, checked clean | 0.15 |
|---|---|
held = coalesce(i.Text, []) then map(held, …) | refused: held is text or an empty list (MOV_COLLECTION_OP_NOT_A_COLLECTION) |
coalesce(n, "n/a") + 1 | refused: a number or a text (MOV_ARITH_NON_NUMERIC); write coalesce(n, 0) + 1 |
A fallback of the value's own kind types as before: coalesce(tags, []) is a list of text.
4. Airtable and Google Sheets cells that can be empty #
Airtable leaves an empty cell out of a row. A field that can be empty is now typed T | null: every kind of text, a number, a date, a single select, a collaborator, a barcode, a formula, a rollup, AI text, the last-modified time and author. A Google Sheets table's every column, and a named or addressed cell's Value, may be absent too.
| 0.14, checked clean | 0.15 |
|---|---|
ops-[v:Vendors WHERE v.Spend > 0]-> | ops-[v:Vendors WHERE v.Spend != null && v.Spend > 0]-> |
write crm-[:Leads]-> { Name: r.Title } | write crm-[:Leads]-> { Name: coalesce(r.Title, "Untitled") }, or Name ?: r.Title to leave it unset |
These stay always there: a checkbox (an empty one reads false), a list field such as a multiple select, attachments or linked rows (an empty one reads []), an autonumber, a count, the created time and the creator. A write still gives a field a value.
5. A List.groupBy or List.keyBy key that may be absent #
The run fails at the first member whose key answers nothing, so such a key is now an error (MOV_DICT_KEY_MAY_BE_ABSENT; a warning in 0.14).
| 0.14, warned | 0.15 |
|---|---|
List.groupBy(rows, (r) => r.Owner) | List.groupBy(rows, (r) => coalesce(r.Owner, "Unassigned")) |
6. The records a block answers come in the walk's order #
A block over a walk with no ORDER BY answers in no order. first, [0], Text.join and reduce over the records it answers are refused (MOV_FOLD_NEEDS_ORDER), as they were over the values it answers. A list literal that spreads a collection in no order is in no order too: a spread keeps the order of what it splices in.
| 0.14, checked clean | 0.15 |
|---|---|
ps = c-[p:People]-> { return p } then first(ps) | order the walk (c-[p:People ORDER BY p.Name]->), or only(…) where exactly one is meant |
Text.join([...names], ", "), names from a walk with no ORDER BY | Text.join(sort(names), ", ") |
7. Inside walk brackets a built-in is spelled as outside them #
A built-in has one spelling: Text.lower(c.Name), Text.trim(…), Number.round(…), coalesce(…), exists(…), inside a walk's WHERE and ORDER BY as in a function body. The walk words 0.14 read there are refused (MOV_V5_SPELLING), and listen-fire upgrade rewrites them: the predicate a run evaluates, and a host receives, is the same. The logic words stay: AND, OR, NOT, CONTAINS, IN inside the brackets.
| 0.14 | 0.15 |
|---|---|
ch-[m:Messages WHERE LOWER(m.Text) CONTAINS "urgent"]-> | ch-[m:Messages WHERE Text.lower(m.Text) CONTAINS "urgent"]-> |
crm-[c:Companies WHERE EXISTS(c-[:Team]->)]-> | crm-[c:Companies WHERE exists(c-[:Team]->)]-> |
ch-[m:Messages WHERE LENGTH(m.Tags) > 0]->, Tags a list | ch-[m:Messages WHERE count(m.Tags) > 0]->, by hand: count reads into a different predicate, so upgrade leaves it |
8. A built-in that can answer nothing says so #
A built-in's answer is T | absent exactly when its run can answer nothing, as TypeScript types a function that can return undefined, and T otherwise. 0.14 typed these answers as always there while the run answered null, so a comparison or a write read the null without a word.
- Given a value that may be absent:
Text.trim,Text.lower,Text.upper,Text.from,Text.slug,Text.pairs,DateTime.at. - Given a value that may be absent, or text, which may read as no date:
Date.from,DateTime.from,DateTime.toIso,Date.addDays,Date.format,Date.daysBetween. - Whatever it is given:
Text.regexExtract(nothing matched),Currency.numberFromFigureandCurrency.codeFromFigure(no figure, no currency).
| 0.14, checked clean | 0.15 |
|---|---|
due = Date.addDays(Date.parse(m.Subject), 7) then due < today | refused (MOV_ABSENT_REQUIRED): test the parsed date first, if (d == null) { fail("…") } |
Description: Currency.codeFromFigure(m.Body) in a write | refused: Description ?: Currency.codeFromFigure(m.Body), or coalesce(…, "unknown") |
Number.format(Currency.numberFromFigure(m.Body), "compact") | refused: coalesce(Currency.numberFromFigure(m.Body), 0), or test it first |
A date or datetime that is there still gives an answer that is there: Date.addDays(Date.today("UTC"), 7) and Date.format(m.Date, "D MMMM") are unchanged. listen-fire upgrade adds no fallback, since the fallback is the author's to choose.
9. A whole program #
A grouping key with a fallback, keys sorted before they are joined, and a record tested before it is read:
gh = connect `@lf/github` using my_github
team = connect `@lf/slack` using team_workspace
function `Open Issues`(go: <manual-[:Invocation]->>) {
authors = gh-[r:Repositories WHERE r.Name == "api"]->-[i:Issues WHERE i.State == "open"]-> { return i.Author }
by = List.groupBy(authors, (who) => coalesce(who, "nobody"))
lines = map(sort(Dict.keys(by)), (who) => "${who}: ${count(by[who])}")
ops = only(team-[ch:Channels WHERE ch.Name == "ops"]->)
if (!exists(ops)) { fail("no #ops channel") }
write ops-[:Messages]-> { Message: Text.join(lines, "\n") }
}
10. Diagnostic code index #
| code | what 0.15 refuses |
|---|---|
MOV_V5_HIDES_BUILTIN | a name that is a word an expression reads as itself |
MOV_COLLECTION_OP_NOT_A_COLLECTION, MOV_ARITH_NON_NUMERIC, MOV_BUILTIN_ARG_TYPE | a coalesce whose union is not what the read needs |
MOV_ABSENT_REQUIRED | an Airtable or Google Sheets field that may be empty, or a built-in's answer that may be absent (section 8), read where a value is needed |
MOV_DICT_KEY_MAY_BE_ABSENT | a grouping key that may be absent (now an error) |
MOV_FOLD_NEEDS_ORDER | first, [0], Text.join or reduce over the records an unordered walk's block answers, or over a list that spreads them |
MOV_V5_SPELLING | a CAPS walk word (LOWER, EXISTS, …) inside walk brackets; rewritten by listen-fire upgrade, except LENGTH of a list, which is count(xs) by hand |