@lotics/cli 0.277.0 → 0.278.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/src/cli.js +1702 -478
  2. package/package.json +1 -1
package/dist/src/cli.js CHANGED
@@ -9054,8 +9054,8 @@ function datetime(args) {
9054
9054
  opts.push("");
9055
9055
  if (args.offset)
9056
9056
  opts.push(`([+-](?:[01]\\d|2[0-3]):[0-5]\\d)`);
9057
- const timeRegex = `${time3}(?:${opts.join("|")})`;
9058
- return new RegExp(`^${dateSource}T(?:${timeRegex})$`);
9057
+ const timeRegex2 = `${time3}(?:${opts.join("|")})`;
9058
+ return new RegExp(`^${dateSource}T(?:${timeRegex2})$`);
9059
9059
  }
9060
9060
  var string = (params) => {
9061
9061
  const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
@@ -10643,8 +10643,8 @@ var $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
10643
10643
  });
10644
10644
  defineLazy(inst._zod, "pattern", () => {
10645
10645
  if (def.options.every((o) => o._zod.pattern)) {
10646
- const patterns = def.options.map((o) => o._zod.pattern);
10647
- return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join("|")})$`);
10646
+ const patterns2 = def.options.map((o) => o._zod.pattern);
10647
+ return new RegExp(`^(${patterns2.map((p) => cleanRegex(p.source)).join("|")})$`);
10648
10648
  }
10649
10649
  return void 0;
10650
10650
  });
@@ -19191,7 +19191,7 @@ var formatMap = {
19191
19191
  var stringProcessor = (schema, ctx, _json, _params) => {
19192
19192
  const json2 = _json;
19193
19193
  json2.type = "string";
19194
- const { minimum, maximum, format: format2, patterns, contentEncoding } = schema._zod.bag;
19194
+ const { minimum, maximum, format: format2, patterns: patterns2, contentEncoding } = schema._zod.bag;
19195
19195
  if (typeof minimum === "number")
19196
19196
  json2.minLength = minimum;
19197
19197
  if (typeof maximum === "number")
@@ -19206,8 +19206,8 @@ var stringProcessor = (schema, ctx, _json, _params) => {
19206
19206
  }
19207
19207
  if (contentEncoding)
19208
19208
  json2.contentEncoding = contentEncoding;
19209
- if (patterns && patterns.size > 0) {
19210
- const regexes = [...patterns];
19209
+ if (patterns2 && patterns2.size > 0) {
19210
+ const regexes = [...patterns2];
19211
19211
  if (regexes.length === 1)
19212
19212
  json2.pattern = regexes[0].source;
19213
19213
  else if (regexes.length > 1) {
@@ -19539,14 +19539,14 @@ var recordProcessor = (schema, ctx, _json, params) => {
19539
19539
  json2.type = "object";
19540
19540
  const keyType = def.keyType;
19541
19541
  const keyBag = keyType._zod.bag;
19542
- const patterns = keyBag?.patterns;
19543
- if (def.mode === "loose" && patterns && patterns.size > 0) {
19542
+ const patterns2 = keyBag?.patterns;
19543
+ if (def.mode === "loose" && patterns2 && patterns2.size > 0) {
19544
19544
  const valueSchema = process2(def.valueType, ctx, {
19545
19545
  ...params,
19546
19546
  path: [...params.path, "patternProperties", "*"]
19547
19547
  });
19548
19548
  json2.patternProperties = {};
19549
- for (const pattern of patterns) {
19549
+ for (const pattern of patterns2) {
19550
19550
  json2.patternProperties[pattern.source] = valueSchema;
19551
19551
  }
19552
19552
  } else {
@@ -21994,6 +21994,181 @@ function toDate(argument, context) {
21994
21994
  return constructFrom(context || argument, argument);
21995
21995
  }
21996
21996
 
21997
+ // ../../node_modules/date-fns/parseISO.js
21998
+ function parseISO(argument, options) {
21999
+ const invalidDate = () => constructFrom(options?.in, NaN);
22000
+ const additionalDigits = options?.additionalDigits ?? 2;
22001
+ const dateStrings = splitDateString(argument);
22002
+ let date5;
22003
+ if (dateStrings.date) {
22004
+ const parseYearResult = parseYear(dateStrings.date, additionalDigits);
22005
+ date5 = parseDate(parseYearResult.restDateString, parseYearResult.year);
22006
+ }
22007
+ if (!date5 || isNaN(+date5)) return invalidDate();
22008
+ const timestamp = +date5;
22009
+ let time3 = 0;
22010
+ let offset;
22011
+ if (dateStrings.time) {
22012
+ time3 = parseTime(dateStrings.time);
22013
+ if (isNaN(time3)) return invalidDate();
22014
+ }
22015
+ if (dateStrings.timezone) {
22016
+ offset = parseTimezone(dateStrings.timezone);
22017
+ if (isNaN(offset)) return invalidDate();
22018
+ } else {
22019
+ const tmpDate = new Date(timestamp + time3);
22020
+ const result = toDate(0, options?.in);
22021
+ result.setFullYear(
22022
+ tmpDate.getUTCFullYear(),
22023
+ tmpDate.getUTCMonth(),
22024
+ tmpDate.getUTCDate()
22025
+ );
22026
+ result.setHours(
22027
+ tmpDate.getUTCHours(),
22028
+ tmpDate.getUTCMinutes(),
22029
+ tmpDate.getUTCSeconds(),
22030
+ tmpDate.getUTCMilliseconds()
22031
+ );
22032
+ return result;
22033
+ }
22034
+ return toDate(timestamp + time3 + offset, options?.in);
22035
+ }
22036
+ var patterns = {
22037
+ dateTimeDelimiter: /[T ]/,
22038
+ timeZoneDelimiter: /[Z ]/i,
22039
+ timezone: /([Z+-].*)$/
22040
+ };
22041
+ var dateRegex = /^-?(?:(\d{3})|(\d{2})(?:-?(\d{2}))?|W(\d{2})(?:-?(\d{1}))?|)$/;
22042
+ var timeRegex = /^(\d{2}(?:[.,]\d*)?)(?::?(\d{2}(?:[.,]\d*)?))?(?::?(\d{2}(?:[.,]\d*)?))?$/;
22043
+ var timezoneRegex = /^([+-])(\d{2})(?::?(\d{2}))?$/;
22044
+ function splitDateString(dateString) {
22045
+ const dateStrings = {};
22046
+ const array2 = dateString.split(patterns.dateTimeDelimiter);
22047
+ let timeString;
22048
+ if (array2.length > 2) {
22049
+ return dateStrings;
22050
+ }
22051
+ if (/:/.test(array2[0])) {
22052
+ timeString = array2[0];
22053
+ } else {
22054
+ dateStrings.date = array2[0];
22055
+ timeString = array2[1];
22056
+ if (patterns.timeZoneDelimiter.test(dateStrings.date)) {
22057
+ dateStrings.date = dateString.split(patterns.timeZoneDelimiter)[0];
22058
+ timeString = dateString.substr(
22059
+ dateStrings.date.length,
22060
+ dateString.length
22061
+ );
22062
+ }
22063
+ }
22064
+ if (timeString) {
22065
+ const token2 = patterns.timezone.exec(timeString);
22066
+ if (token2) {
22067
+ dateStrings.time = timeString.replace(token2[1], "");
22068
+ dateStrings.timezone = token2[1];
22069
+ } else {
22070
+ dateStrings.time = timeString;
22071
+ }
22072
+ }
22073
+ return dateStrings;
22074
+ }
22075
+ function parseYear(dateString, additionalDigits) {
22076
+ const regex = new RegExp(
22077
+ "^(?:(\\d{4}|[+-]\\d{" + (4 + additionalDigits) + "})|(\\d{2}|[+-]\\d{" + (2 + additionalDigits) + "})$)"
22078
+ );
22079
+ const captures = dateString.match(regex);
22080
+ if (!captures) return { year: NaN, restDateString: "" };
22081
+ const year = captures[1] ? parseInt(captures[1]) : null;
22082
+ const century = captures[2] ? parseInt(captures[2]) : null;
22083
+ return {
22084
+ year: century === null ? year : century * 100,
22085
+ restDateString: dateString.slice((captures[1] || captures[2]).length)
22086
+ };
22087
+ }
22088
+ function parseDate(dateString, year) {
22089
+ if (year === null) return /* @__PURE__ */ new Date(NaN);
22090
+ const captures = dateString.match(dateRegex);
22091
+ if (!captures) return /* @__PURE__ */ new Date(NaN);
22092
+ const isWeekDate = !!captures[4];
22093
+ const dayOfYear = parseDateUnit(captures[1]);
22094
+ const month = parseDateUnit(captures[2]) - 1;
22095
+ const day = parseDateUnit(captures[3]);
22096
+ const week = parseDateUnit(captures[4]);
22097
+ const dayOfWeek = parseDateUnit(captures[5]) - 1;
22098
+ if (isWeekDate) {
22099
+ if (!validateWeekDate(year, week, dayOfWeek)) {
22100
+ return /* @__PURE__ */ new Date(NaN);
22101
+ }
22102
+ return dayOfISOWeekYear(year, week, dayOfWeek);
22103
+ } else {
22104
+ const date5 = /* @__PURE__ */ new Date(0);
22105
+ if (!validateDate(year, month, day) || !validateDayOfYearDate(year, dayOfYear)) {
22106
+ return /* @__PURE__ */ new Date(NaN);
22107
+ }
22108
+ date5.setUTCFullYear(year, month, Math.max(dayOfYear, day));
22109
+ return date5;
22110
+ }
22111
+ }
22112
+ function parseDateUnit(value) {
22113
+ return value ? parseInt(value) : 1;
22114
+ }
22115
+ function parseTime(timeString) {
22116
+ const captures = timeString.match(timeRegex);
22117
+ if (!captures) return NaN;
22118
+ const hours = parseTimeUnit(captures[1]);
22119
+ const minutes = parseTimeUnit(captures[2]);
22120
+ const seconds = parseTimeUnit(captures[3]);
22121
+ if (!validateTime(hours, minutes, seconds)) {
22122
+ return NaN;
22123
+ }
22124
+ return hours * millisecondsInHour + minutes * millisecondsInMinute + seconds * 1e3;
22125
+ }
22126
+ function parseTimeUnit(value) {
22127
+ return value && parseFloat(value.replace(",", ".")) || 0;
22128
+ }
22129
+ function parseTimezone(timezoneString) {
22130
+ if (timezoneString === "Z") return 0;
22131
+ const captures = timezoneString.match(timezoneRegex);
22132
+ if (!captures) return 0;
22133
+ const sign = captures[1] === "+" ? -1 : 1;
22134
+ const hours = parseInt(captures[2]);
22135
+ const minutes = captures[3] && parseInt(captures[3]) || 0;
22136
+ if (!validateTimezone(hours, minutes)) {
22137
+ return NaN;
22138
+ }
22139
+ return sign * (hours * millisecondsInHour + minutes * millisecondsInMinute);
22140
+ }
22141
+ function dayOfISOWeekYear(isoWeekYear, week, day) {
22142
+ const date5 = /* @__PURE__ */ new Date(0);
22143
+ date5.setUTCFullYear(isoWeekYear, 0, 4);
22144
+ const fourthOfJanuaryDay = date5.getUTCDay() || 7;
22145
+ const diff = (week - 1) * 7 + day + 1 - fourthOfJanuaryDay;
22146
+ date5.setUTCDate(date5.getUTCDate() + diff);
22147
+ return date5;
22148
+ }
22149
+ var daysInMonths = [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
22150
+ function isLeapYearIndex(year) {
22151
+ return year % 400 === 0 || year % 4 === 0 && year % 100 !== 0;
22152
+ }
22153
+ function validateDate(year, month, date5) {
22154
+ return month >= 0 && month <= 11 && date5 >= 1 && date5 <= (daysInMonths[month] || (isLeapYearIndex(year) ? 29 : 28));
22155
+ }
22156
+ function validateDayOfYearDate(year, dayOfYear) {
22157
+ return dayOfYear >= 1 && dayOfYear <= (isLeapYearIndex(year) ? 366 : 365);
22158
+ }
22159
+ function validateWeekDate(_year, week, day) {
22160
+ return week >= 1 && week <= 53 && day >= 0 && day <= 6;
22161
+ }
22162
+ function validateTime(hours, minutes, seconds) {
22163
+ if (hours === 24) {
22164
+ return minutes === 0 && seconds === 0;
22165
+ }
22166
+ return seconds >= 0 && seconds < 60 && minutes >= 0 && minutes < 60 && hours >= 0 && hours < 25;
22167
+ }
22168
+ function validateTimezone(_hours, minutes) {
22169
+ return minutes >= 0 && minutes <= 59;
22170
+ }
22171
+
21997
22172
  // ../../node_modules/date-fns/isDate.js
21998
22173
  function isDate(value) {
21999
22174
  return value instanceof Date || typeof value === "object" && Object.prototype.toString.call(value) === "[object Date]";
@@ -22052,6 +22227,9 @@ function differenceInCalendarDays(laterDate, earlierDate, options) {
22052
22227
  return Math.round((laterTimestamp - earlierTimestamp) / millisecondsInDay);
22053
22228
  }
22054
22229
 
22230
+ // ../shared/src/formula_helpers.ts
22231
+ var FORMULA_ERROR_PREFIX = "#ERROR: ";
22232
+
22055
22233
  // ../shared/src/aggregate.ts
22056
22234
  var baseAggregateOperationSchema = external_exports.enum([
22057
22235
  "empty",
@@ -22251,6 +22429,24 @@ function keepsAggregatedUnit(operation) {
22251
22429
  function rollupOutputType(operation) {
22252
22430
  return operation === "earliest" || operation === "latest" ? "date" : "number";
22253
22431
  }
22432
+ function rollupOutputFormat(operation, aggregated, rowSelect = {}) {
22433
+ if (keepsAggregatedUnit(operation)) {
22434
+ return {
22435
+ aggregate_field_format: aggregated.format,
22436
+ aggregate_field_currency: aggregated.currency,
22437
+ aggregate_field_unit: aggregated.unit,
22438
+ aggregate_field_unit_field: rowSelect.unit_field,
22439
+ aggregate_field_currency_field: rowSelect.currency_field
22440
+ };
22441
+ }
22442
+ return {
22443
+ aggregate_field_format: operation.startsWith("percent_") ? "percentage" : "number",
22444
+ aggregate_field_currency: void 0,
22445
+ aggregate_field_unit: void 0,
22446
+ aggregate_field_unit_field: void 0,
22447
+ aggregate_field_currency_field: void 0
22448
+ };
22449
+ }
22254
22450
  var OPERATION_FIELD_TYPES = {
22255
22451
  empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
22256
22452
  filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
@@ -25719,6 +25915,12 @@ function isPlatformWrittenField(field) {
25719
25915
  }
25720
25916
 
25721
25917
  // ../shared/src/binding_keys.ts
25918
+ function fieldKeyName(entityAlias, fieldAlias2) {
25919
+ return `${entityAlias}.${fieldAlias2}`;
25920
+ }
25921
+ function optionKeyName(entityAlias, fieldAlias2, optionAlias) {
25922
+ return `${entityAlias}.${fieldAlias2}:${optionAlias}`;
25923
+ }
25722
25924
  function parseFieldKeyName(key) {
25723
25925
  const dot = key.indexOf(".");
25724
25926
  if (dot <= 0) return null;
@@ -25728,6 +25930,14 @@ function parseFieldKeyName(key) {
25728
25930
  if (field === "" || field.includes(".") || field.includes(":")) return null;
25729
25931
  return { entity, field };
25730
25932
  }
25933
+ function parseOptionKeyName(key) {
25934
+ const colon = key.indexOf(":");
25935
+ if (colon <= 0) return null;
25936
+ const option = key.slice(colon + 1);
25937
+ if (option === "" || option.includes(":") || option.includes(".")) return null;
25938
+ const field = parseFieldKeyName(key.slice(0, colon));
25939
+ return field === null ? null : { ...field, option };
25940
+ }
25731
25941
 
25732
25942
  // ../shared/src/formula_references.ts
25733
25943
  var FIELD_REF_TOKEN = /\{([^{}]+)\}/g;
@@ -25768,6 +25978,9 @@ function prepareFormulaExpression(expression, fields) {
25768
25978
  unresolved
25769
25979
  };
25770
25980
  }
25981
+ function referencedFieldKeys(expression, fields) {
25982
+ return prepareFormulaExpression(expression, fields).referencedKeys;
25983
+ }
25771
25984
  var CONTRACT_OPTION_REF = /^([a-z][a-z0-9_]*):([a-z][a-z0-9_]*)$/;
25772
25985
  function contractOptionRef(token2) {
25773
25986
  const match2 = CONTRACT_OPTION_REF.exec(token2);
@@ -30420,6 +30633,17 @@ function parseWholeExpression(expression, astFactory2) {
30420
30633
  function parseExpression(expression) {
30421
30634
  return parseWholeExpression(expression, astFactory);
30422
30635
  }
30636
+ function evaluateJsExpression(expression, data, additionalFunctions, defaultTimezone) {
30637
+ const timezoneAwareFunctions = createTimezoneAwareFunctions(defaultTimezone);
30638
+ const expr = parseWholeExpression(expression, astFactory);
30639
+ const result = expr?.evaluate({
30640
+ ...data,
30641
+ ...timezoneAwareFunctions,
30642
+ ...additionalFunctions
30643
+ });
30644
+ return result;
30645
+ }
30646
+ var EMPTINESS_PREDICATES = /* @__PURE__ */ new Set(["isEmpty", "isNull", "isNotNull"]);
30423
30647
  function createTimezoneAwareFunctions(defaultTimezone) {
30424
30648
  return {
30425
30649
  // Number functions
@@ -30870,6 +31094,27 @@ function resolvedFieldType(field, entity, entities, hops = 0) {
30870
31094
  return field.type;
30871
31095
  }
30872
31096
  }
31097
+ function holdsMoment(field, entity, entities, hops = 0) {
31098
+ if (hops >= RESULT_HOPS) return false;
31099
+ switch (field.type) {
31100
+ case "date":
31101
+ return field.format === "datetime";
31102
+ case "formula":
31103
+ return field.formula.output_type === "datetime";
31104
+ case "lookup": {
31105
+ const target = linkTarget(entity, entities, field.source_field_alias);
31106
+ const shown = target?.fields.find((candidate) => candidate.alias === field.lookup_field_alias);
31107
+ return target !== void 0 && shown !== void 0 && holdsMoment(shown, target, entities, hops + 1);
31108
+ }
31109
+ case "rollup": {
31110
+ const target = linkTarget(entity, entities, field.source_field_alias);
31111
+ const aggregated = target?.fields.find((candidate) => candidate.alias === field.aggregate_option.field_key);
31112
+ return target !== void 0 && aggregated !== void 0 && holdsMoment(aggregated, target, entities, hops + 1);
31113
+ }
31114
+ default:
31115
+ return false;
31116
+ }
31117
+ }
30873
31118
  var PLURAL_VOCABULARY = { unit: "units", currency: "currencies" };
30874
31119
  function contractRollupSelect(entity, link, child, select) {
30875
31120
  const lookup = child.fields.find((field) => field.alias === select);
@@ -32005,7 +32250,7 @@ var MODEL_RULES = {
32005
32250
  sentence: "A label is unique where apply finds it by label: entities, roles and templates in the model, fields and views in their entity, options in their select \u2014 and no view takes its entity's own label, which names the whole-table grid apply makes."
32006
32251
  },
32007
32252
  "model.template-sha": { sentence: "A template's `content_sha256`, where stated, is the 64-character lowercase hex sha256 of its content." },
32008
- "model.template-kind": { sentence: "An act makes its document from an html template; a register's `export` fills an html or an excel one." },
32253
+ "model.template-kind": { sentence: "An act's paper and a register's `export` are made from an html or an excel template, never an email one." },
32009
32254
  "register.export-report": {
32010
32255
  sentence: "A register's reports each have their own label; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes."
32011
32256
  },
@@ -32070,6 +32315,9 @@ var MODEL_RULES = {
32070
32315
  "records.milestones": { sentence: "Milestones are dates a person ticks, each named once; `closed` names some of them, and a deadline (`due`) is never one." },
32071
32316
  "records.milestone-add": { sentence: "An add asks the first milestone at most \u2014 each after it is ticked in order." },
32072
32317
  "records.figure": { sentence: "A figure is a number." },
32318
+ "records.applies": {
32319
+ sentence: "`applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`."
32320
+ },
32073
32321
  "records.limits": {
32074
32322
  sentence: "`limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit."
32075
32323
  },
@@ -32094,6 +32342,7 @@ var MODEL_RULES = {
32094
32342
  "write.bound-per-row-unit": {
32095
32343
  sentence: "On a figure whose unit or currency is the row's own (`unit_field`, `currency_field`) a constant bound is only 0 \u2014 bound it by a field of the row."
32096
32344
  },
32345
+ "write.suggest": { sentence: "`suggest` stands on a stored text field and names `<entity>.<field>`, a stored text field of a declared entity; the app reads that entity, so it has a `records` entry." },
32097
32346
  "write.same": { sentence: "`same` stands on a link and names one-row links this entity and the link's target both hold to one entity." },
32098
32347
  "write.options-where": {
32099
32348
  sentence: "`options_where` narrows a link by an `and` group of plain conditions over the target's own fields \u2014 a number compared in one unit on every row, a select by options it declares."
@@ -32127,7 +32376,7 @@ var MODEL_RULES = {
32127
32376
  },
32128
32377
  "app.row": { sentence: "An app's `row` states only what differs from `records`: a line that is not the title, a number for its figure, a files picture no act makes documents into." },
32129
32378
  "app.files-shown": {
32130
- sentence: "Every files field is drawn by some app over its entity \u2014 a section's `fields`, a `files` block, a column, the `image`, or where an act making papers into it stands: the field an act keeps its document in (`into`) is placed where the paper reads."
32379
+ sentence: "Every files field is drawn by some app over its entity \u2014 a section's `fields`, a `files` block, a column, the `image`, or where an act making its papers into it (`into`) stands, a section naming the act: the made paper reads under the act, one per template."
32131
32380
  },
32132
32381
  "app.document-image": { sentence: "A linked entity holding one files field and no `image`: `image` lets a link preview its paper.", note: true },
32133
32382
  "app.tasks": {
@@ -32155,7 +32404,7 @@ var MODEL_RULES = {
32155
32404
  sentence: "A register's `tabs` each name a tab of their own, never a key a report already holds; a tab's `in` and `open` dates are dates of its rows, `open`'s two different; its `where` and `sort` hold as the register's do, and never narrow by the status, which its chips split. Only a table or cards reads tabs, and a band reading's `tab` names one of them."
32156
32405
  },
32157
32406
  "register.group": {
32158
- sentence: "`group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and grouping by the title needs a subtitle to lead each row."
32407
+ sentence: "`group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and a gantt into its lanes; grouping by the title needs a subtitle to lead each row."
32159
32408
  },
32160
32409
  "register.readings-own": { sentence: "A register's `readings` read its own entity; another entity's stand on a dashboard." },
32161
32410
  "register.readings-restate": {
@@ -32168,6 +32417,9 @@ var MODEL_RULES = {
32168
32417
  sentence: '`layout: "roster"` and `roster` go together: the roster\'s entity has one single link to this one and a date on its line, a roster draws no `columns`, `expect` needs `roster` and `of` needs `expect`.'
32169
32418
  },
32170
32419
  "register.roster-expect": { sentence: "A roster's `expect` is a single select of its rows, and `of` this entity's multi-select whose every option is one of `expect`'s." },
32420
+ "register.gantt": {
32421
+ sentence: '`layout: "gantt"` and `gantt` go together: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each milestone another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity.'
32422
+ },
32171
32423
  "register.lanes": {
32172
32424
  sentence: '`layout: "lanes"` and `lanes` go together: `lanes` is a one-row link to an entity with a `records` entry, the rows name a date on their line, a block\'s two ends are read alike (two days or two moments), and `loads` needs `lanes`.'
32173
32425
  },
@@ -32199,8 +32451,12 @@ var MODEL_RULES = {
32199
32451
  "record.section-holds": { sentence: "A section holds fields, blocks or acts." },
32200
32452
  "record.at": { sentence: "`at` names options of the record's stored status, or its milestones; a record with no status has no steps." },
32201
32453
  "record.page": {
32202
- sentence: "A section is a `page` only on a page door and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section."
32454
+ sentence: "A section is a `page` only on a page door \u2014 never a drawer's or one beside the list \u2014 and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section."
32455
+ },
32456
+ "record.side": {
32457
+ sentence: "At most one section stands beside the rest (`side`), on a page door, and never also a page of its own."
32203
32458
  },
32459
+ "record.beside": { sentence: 'A `door: "beside"` record opens beside a table register \u2014 never a board, cards or a calendar, which draw no list to stand beside.' },
32204
32460
  "record.history": { sentence: "`record.history` draws a status history the entity records (`status.history`); no block reads that history." },
32205
32461
  "record.section-acts": {
32206
32462
  sentence: "A section's `acts` name acts of the record itself: an act of a child (`of`) stands on the child's row, and one over the register's rows (`on`) on the register."
@@ -32228,6 +32484,18 @@ var MODEL_RULES = {
32228
32484
  sentence: "`under` names the child's one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines' columns never read the document (its heading does) nor hold long text."
32229
32485
  },
32230
32486
  "block.under-once": { sentence: "Documents drawn over their lines (`under`) are never listed again as a block of their own." },
32487
+ "block.edits": {
32488
+ sentence: "A rows block's `edits` name at most two of the child's columns or its figure that the row's own save writes \u2014 a number, a date, a few words or one option \u2014 never on an `expect` block, whose lines are filled in place already."
32489
+ },
32490
+ "register.remove": {
32491
+ sentence: "A register's `remove: false` hides the Delete its records would offer, so it stands only where the app writes its records."
32492
+ },
32493
+ "block.remove": {
32494
+ sentence: "A rows block's `remove: false` hides the Delete its rows would offer, so it stands only where the app writes those rows."
32495
+ },
32496
+ "block.narrow": {
32497
+ sentence: "A rows block's `filters`, `search`, `group` and `sort` follow the register's rules over the child's rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, never on an `expect` block, and an `under` block is grouped by its documents."
32498
+ },
32231
32499
  "block.expect": {
32232
32500
  sentence: "`expect` names the child's title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the block; the block's `columns` name at least one field a person fills in place, and its `create` takes the file where the child is pictured by one."
32233
32501
  },
@@ -32244,7 +32512,12 @@ var MODEL_RULES = {
32244
32512
  "block.agenda-note": { sentence: "Rows each on a day and within it (at an hour, in a part of the day) listed as a table: an `agenda` draws them by day.", note: true },
32245
32513
  "block.under-note": { sentence: "Rows holding a paper that cover another block's lines, listed apart with no add of their own stated: `under` draws them over the lines they cover.", note: true },
32246
32514
  // ─── Acts ────────────────────────────────────────────────────────────────────
32247
- "act.set-not-asked": { sentence: "A field an act `set`s is the act's to write: read-only everywhere else and never asked by an add (`create`) \u2014 a required one takes a `default`." },
32515
+ "act.set-not-asked": {
32516
+ sentence: "A field an act `set`s \u2014 an option, a value, `now`, `me`, an input \u2014 is the act's outcome: read-only everywhere else and never asked by an add (`create`); a required one takes a `default`. A field an act computes by a `formula` is a starting value, which a person may change after."
32517
+ },
32518
+ "act.set-formula": {
32519
+ sentence: "`set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text or a yes/no, typed as the entity's own formulas are \u2014 never the status or a milestone; a bound a write rule holds the field to is checked on the computed value as the act runs."
32520
+ },
32248
32521
  "act.of": {
32249
32522
  sentence: 'An act\'s `of` names a child the record lists in a rows block; it works on the one child row it is pressed on \u2014 or, `on: "rows"`, on the rows ticked together where the record lists that child as tasks \u2014 never on the rows in view.'
32250
32523
  },
@@ -32294,7 +32567,7 @@ var error51 = (rule, path15, message2) => ({ severity: "error", path: path15, me
32294
32567
  var note = (rule, path15, message2) => ({ severity: "note", path: path15, message: message2, rule });
32295
32568
 
32296
32569
  // ../shared/src/schemas/app_spec_version.ts
32297
- var APP_SPEC_VERSION = 11;
32570
+ var APP_SPEC_VERSION = 12;
32298
32571
 
32299
32572
  // ../shared/src/schemas/app_query_aggregate.ts
32300
32573
  var AGGREGATE_MAX_BY = 3;
@@ -32392,6 +32665,9 @@ var specFieldSchema = zod_default.object({
32392
32665
  * does: nothing accumulated yet is zero, which a meter draws — any other computed value holding nothing has no
32393
32666
  * answer. */
32394
32667
  accumulates: zod_default.literal(true).optional(),
32668
+ /** A formula in a fixed currency or unit that computes this figure of the same row again where the row's own
32669
+ * currency or unit (its `currency_field`, `unit_field`) is that one — a conversion: blank on such a row. */
32670
+ restates: specColumnAliasSchema.optional(),
32395
32671
  /** What an add starts at where the model says it is certain: a date at today (a moment at now), a
32396
32672
  * member at the reader adding the row. */
32397
32673
  starts: zod_default.enum(["today", "me"]).optional(),
@@ -32426,7 +32702,15 @@ var specFieldSchema = zod_default.object({
32426
32702
  /** The read a link's picker lists from, where the model narrows which rows it may point at; absent, the entity's `list`.
32427
32703
  * `same`: the one-row links of this row whose row the picked one must name too, each handed to the read in
32428
32704
  * its `param` — the picker offers nothing until the row names it. */
32429
- pick: specReadSchema.extend({ same: zod_default.array(zod_default.object({ field: specColumnAliasSchema, param: zod_default.string().min(1) }).strict()).min(1).optional() }).strict().optional()
32705
+ pick: specReadSchema.extend({ same: zod_default.array(zod_default.object({ field: specColumnAliasSchema, param: zod_default.string().min(1) }).strict()).min(1).optional() }).strict().optional(),
32706
+ /** A text field's control offers, as the reader types, the values this read answers — a group read, one row per
32707
+ * distinct value under `column` with `__count`, most held first, `param` taking what the reader typed. Any
32708
+ * text is still written. */
32709
+ suggest: specReadSchema.extend({ param: zod_default.string().min(1), column: specColumnAliasSchema }).strict().optional(),
32710
+ /** It applies only while each condition holds on its row — where it does not, no add asks it and no surface shows it.
32711
+ * Each names a column of the same row: a yes/no or single select, or a lookup of one through a one-row link
32712
+ * (`through`), whose value a form reads off the linked row once picked — the link's `face` carries it. */
32713
+ applies: specWhenSchema.min(1).optional()
32430
32714
  }).strict();
32431
32715
  var specTaskMoveSchema = zod_default.object({ to: specOptionIdSchema, act: contractAliasSchema.optional() }).strict();
32432
32716
  var specTaskSchema = zod_default.object({
@@ -32597,6 +32881,21 @@ var specLanesSchema = zod_default.object({
32597
32881
  * taking the folder as the register's list does; absent, every row the link may point at. */
32598
32882
  read: specReadSchema.extend({ param: zod_default.string().min(1) }).strict().optional()
32599
32883
  }).strict();
32884
+ var specGanttSchema = zod_default.object({
32885
+ /** Where each bar starts and ends: two dates (an axis of days) or two datetimes (of moments). A bar moves, and its
32886
+ * end stretches, where `entities[entity].update` exists and that field is not `readonly` — the update writes it. */
32887
+ start: specColumnAliasSchema,
32888
+ end: specColumnAliasSchema,
32889
+ /** Other dates of the row, each a diamond on its bar's line named by its label. */
32890
+ milestones: zod_default.array(specColumnAliasSchema).min(1).optional(),
32891
+ /** The plan the bar is read against, a thin bar under it; a date not named is the bar's own. */
32892
+ planned: zod_default.object({ start: specColumnAliasSchema.optional(), end: specColumnAliasSchema.optional() }).strict().refine((planned) => planned.start !== void 0 || planned.end !== void 0, { error: "a gantt's plan names its start, its end, or both" }).optional(),
32893
+ /** A percent column (0–100) filling the bar; absent, the bar wears the row's status tone. */
32894
+ progress: specColumnAliasSchema.optional(),
32895
+ /** A link column to rows of the register's own entity each row waits on, finish-to-start: an arrow from each in view. */
32896
+ after: specColumnAliasSchema.optional()
32897
+ }).strict();
32898
+ var specFilterSchema = zod_default.union([specColumnAliasSchema, zod_default.object({ field: specColumnAliasSchema, tiers: zod_default.array(zod_default.number()).min(1) }).strict()]);
32600
32899
  var registerPeriodDaysSchema = zod_default.object({ days: zod_default.number().int().min(1).max(REGISTER_PERIOD_DAYS_MAX).describe(`How many days through today, at most ${REGISTER_PERIOD_DAYS_MAX}`) }).strict();
32601
32900
  var registerPeriodSchema = zod_default.union([zod_default.enum(REGISTER_PERIODS), registerPeriodDaysSchema]);
32602
32901
  var specSortSchema = zod_default.array(zod_default.object({ field: specColumnAliasSchema, desc: zod_default.literal(true).optional() }).strict()).min(1);
@@ -32612,9 +32911,7 @@ var specTabSchema = zod_default.object({
32612
32911
  }).strict();
32613
32912
  var specRegisterSchema = zod_default.object({
32614
32913
  columns: zod_default.array(specColumnAliasSchema),
32615
- /** Each a chip on the band: a column, or a number read in the bands its ascending `tiers` cut — under the first,
32616
- * from each to the next, from the last up. */
32617
- filters: zod_default.array(zod_default.union([specColumnAliasSchema, zod_default.object({ field: specColumnAliasSchema, tiers: zod_default.array(zod_default.number()).min(1) }).strict()])),
32914
+ filters: zod_default.array(specFilterSchema),
32618
32915
  /** The columns the search box matches, a link by its display — each line of a pasted list on its own. Absent, the list read's `search` param. */
32619
32916
  search: zod_default.array(specColumnAliasSchema).min(1).optional(),
32620
32917
  /** The keys rows open ordered by, most significant first, each breaking the ties of the ones before it. */
@@ -32625,17 +32922,22 @@ var specRegisterSchema = zod_default.object({
32625
32922
  tabs: zod_default.array(specTabSchema).min(2).optional(),
32626
32923
  /** The period the tabs open on; absent, this month so far. */
32627
32924
  period: registerPeriodSchema.optional(),
32628
- layout: zod_default.enum(["table", "cards", "calendar", "roster", "lanes"]),
32925
+ layout: zod_default.enum(["table", "cards", "calendar", "roster", "lanes", "gantt"]),
32629
32926
  /** A `roster` layout's cells. */
32630
32927
  roster: specRosterSchema.optional(),
32631
32928
  /** A `lanes` layout's lanes and the dates its blocks run between. */
32632
32929
  lanes: specLanesSchema.optional(),
32930
+ /** A `gantt` layout's bars. */
32931
+ gantt: specGanttSchema.optional(),
32633
32932
  /** The rows this app works — its entity's `list` read keeps them on the server; an add writes a select holding
32634
32933
  * one option and a stored yes/no here, and offers only these options of a select holding several. */
32635
32934
  where: specWhenSchema.optional(),
32636
32935
  /** The narrowing the register opens on while its address states none of it — each a chip the reader may take
32637
32936
  * away; one on the status is the chips' opening choice, a member entry the rows the reader's. */
32638
32937
  opens: specNarrowingSchema.optional(),
32938
+ /** The register's records offer no Delete — neither a row's ⋯ nor its record's — though `entities[entity].remove`
32939
+ * stands where another list of them in this app offers it. */
32940
+ remove: zod_default.literal(false).optional(),
32639
32941
  /** What an add from the register asks; absent, rows are not added here. */
32640
32942
  create: zod_default.array(specColumnAliasSchema).min(1).optional(),
32641
32943
  /**
@@ -32670,6 +32972,9 @@ var specRegisterSchema = zod_default.object({
32670
32972
  if (ctx.value.layout === "lanes" !== (ctx.value.lanes !== void 0)) {
32671
32973
  ctx.issues.push({ code: "custom", input: ctx.value.lanes, path: ["lanes"], message: "a `lanes` layout and its `lanes` come together" });
32672
32974
  }
32975
+ if (ctx.value.layout === "gantt" !== (ctx.value.gantt !== void 0)) {
32976
+ ctx.issues.push({ code: "custom", input: ctx.value.gantt, path: ["gantt"], message: "a `gantt` layout and its `gantt` come together" });
32977
+ }
32673
32978
  });
32674
32979
  var specTimelineBlockSchema = zod_default.object({
32675
32980
  kind: zod_default.literal("timeline"),
@@ -32715,6 +33020,21 @@ var specBlockSchema = zod_default.discriminatedUnion("kind", [
32715
33020
  /** The child's selects each row here holds one of these options of: its read keeps only them, and a
32716
33021
  * row added here is written holding them. */
32717
33022
  where: zod_default.array(specOptionsHeldSchema).optional(),
33023
+ /** The block's chips, as the register's `filters` — narrowing the rows its read keeps, as the register's do. */
33024
+ filters: zod_default.array(specFilterSchema).min(1).optional(),
33025
+ /** The columns the block's search box matches, as the register's `search`; absent, the block has no search box. */
33026
+ search: zod_default.array(specColumnAliasSchema).min(1).optional(),
33027
+ /** The column the rows open grouped by, as the register's `group`. */
33028
+ group: specColumnAliasSchema.optional(),
33029
+ /** The order the rows open in, as the register's `sort` — within each group; its read is ordered so. Absent, the
33030
+ * read's own order. */
33031
+ sort: specSortSchema.optional(),
33032
+ /** The columns each line holds as its control, written in place through the child's update; every other
33033
+ * value reads. */
33034
+ edits: zod_default.array(specColumnAliasSchema).min(1).optional(),
33035
+ /** Its rows offer no Delete here — neither in their ⋯ nor where they open; another list of them or their own
33036
+ * record deletes them. */
33037
+ remove: zod_default.literal(false).optional(),
32718
33038
  /** The document the lines stand under, each a heading over the lines it covers. A document is made here
32719
33039
  * where `entities[entity].create` is: the lines picked are sent as `back`'s pieces added, the record in
32720
33040
  * `via`, and `where` as it states — its other fields are the form's. */
@@ -32786,10 +33106,13 @@ var specSectionSchema = zod_default.object({
32786
33106
  /** A page of its own at `/<record id>/<page>`, off the record's scroll: its own heading and a way back,
32787
33107
  * reached from the rail after the scroll's sections, or from a link at its place where the record has no
32788
33108
  * rail. Its title folded to an address segment, unique among the record's pages; only on a page door, never a step. */
32789
- page: zod_default.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).optional()
33109
+ page: zod_default.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/).optional(),
33110
+ /** Drawn in the pane beside the other sections — what they are worked from; at most one, on a page door. */
33111
+ side: zod_default.literal(true).optional()
32790
33112
  }).strict();
32791
33113
  var specRecordSchema = zod_default.object({
32792
- door: zod_default.enum(["page", "drawer"]),
33114
+ /** `beside`: the register's list and the open record side by side — over a table register alone. */
33115
+ door: zod_default.enum(["page", "drawer", "beside"]),
32793
33116
  sections: zod_default.array(specSectionSchema),
32794
33117
  /** The record's comment thread stands beside its sections. */
32795
33118
  comments: zod_default.literal(true).optional(),
@@ -32803,12 +33126,15 @@ var specRecordSchema = zod_default.object({
32803
33126
  if (history === true && moves === void 0) {
32804
33127
  ctx.issues.push({ code: "custom", input: history, path: ["history"], message: "draws a status history it does not read \u2014 `moves` is the read" });
32805
33128
  }
33129
+ const sides = sections.flatMap((section, index) => section.side === true ? [index] : []);
33130
+ if (sides.length > 0 && door !== "page") ctx.issues.push({ code: "custom", input: door, path: ["door"], message: "a record with a section beside the rest opens on a page" });
33131
+ for (const index of sides.slice(1)) ctx.issues.push({ code: "custom", input: true, path: ["sections", index, "side"], message: "one section stands beside the rest" });
32806
33132
  const seen = /* @__PURE__ */ new Set();
32807
33133
  sections.forEach((section, index) => {
32808
33134
  const page = section.page;
32809
33135
  if (page === void 0) return;
32810
33136
  const refuse = (message2) => ctx.issues.push({ code: "custom", input: page, path: ["sections", index, "page"], message: message2 });
32811
- if (door === "drawer") refuse("a drawer has no pages");
33137
+ if (door !== "page") refuse(`a ${door} door has no pages`);
32812
33138
  if (section.at !== void 0) refuse("a step is read on the record's path, never a page of its own");
32813
33139
  if (seen.has(page)) refuse(`two sections are the page "${page}"`);
32814
33140
  seen.add(page);
@@ -33004,6 +33330,14 @@ var recordDisplaySchema = zod_default.object({
33004
33330
  task: zod_default.literal(true).optional().describe(
33005
33331
  "A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record's rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed`"
33006
33332
  ),
33333
+ applies: zod_default.record(
33334
+ fieldAlias,
33335
+ conditionsHold(
33336
+ "The conditions it applies under, every entry ANDed: a yes/no of the row (stored, or a formula) holding this value, or a single select holding one of these options \u2014 the row's own, or looked up through a one-row link of the row, decided once the link is picked"
33337
+ )
33338
+ ).optional().describe(
33339
+ "Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default`"
33340
+ ),
33007
33341
  starts: zod_default.record(fieldAlias, zod_default.enum(["today", "me"])).optional().describe(
33008
33342
  "What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default`"
33009
33343
  )
@@ -33078,6 +33412,8 @@ var tieredFilterSchema = zod_default.object({
33078
33412
  ctx.issues.push({ code: "custom", input: ctx.value, path: ["tiers"], message: "tiers ascend, each above the one before it" });
33079
33413
  }
33080
33414
  });
33415
+ var LINE_EDITS = 2;
33416
+ var lineFilters = (owner) => zod_default.array(zod_default.union([fieldAlias, tieredFilterSchema])).max(LINE_FILTERS, { error: `${owner} states at most ${LINE_FILTERS} filters \u2014 keep the ones narrowed by daily` });
33081
33417
  var sortKeySchema = zod_default.object({
33082
33418
  field: fieldAlias.describe("The field rows are ordered by"),
33083
33419
  desc: zod_default.literal(true).optional().describe("Latest or largest first; absent, soonest or smallest first")
@@ -33098,9 +33434,20 @@ var registerTabSchema = zod_default.object({
33098
33434
  where: narrowingHolds('Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed').optional(),
33099
33435
  sort: sortSchema.optional().describe("The order this tab's rows read in, as the register's `sort`; absent, the register's")
33100
33436
  }).strict().refine((tab) => tab.in === void 0 || tab.open === void 0, { error: "a tab holds the rows dated `in` the period or those `open` at its end \u2014 state one" });
33437
+ var registerGanttSchema = zod_default.object({
33438
+ start: fieldAlias.describe("A date of this entity each row's bar starts at (a sailing, a hire's start, a task's start)"),
33439
+ end: fieldAlias.optional().describe("The date each row's bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity's first `due`"),
33440
+ milestones: zod_default.array(fieldAlias).min(1).optional().describe("Dates of the row marked on its bar's line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end`"),
33441
+ planned: zod_default.object({
33442
+ start: fieldAlias.optional().describe("The date the row was planned to start at"),
33443
+ end: fieldAlias.optional().describe("The date the row was planned to end at (a booked arrival)")
33444
+ }).strict().refine((planned) => planned.start !== void 0 || planned.end !== void 0, { error: "a plan names the date it was to start at, end at, or both" }).optional().describe("The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar's own"),
33445
+ progress: fieldAlias.optional().describe("A percent of this entity (0\u2013100) filling each row's bar as far as the work is done; absent, the bar wears its status's tone"),
33446
+ after: fieldAlias.optional().describe("A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone")
33447
+ }).strict();
33101
33448
  var registerSchema = zod_default.object({
33102
33449
  columns: zod_default.array(fieldAlias).optional().describe("Fields read as columns after the row's title \u2014 on a calendar's entry or a lane's block, its facts after its title; never what the row draws itself (its subtitle, image, status, figure, or the bound its figure's meter reads against), which the runtime places"),
33103
- filters: zod_default.array(zod_default.union([fieldAlias, tieredFilterSchema])).max(LINE_FILTERS, { error: `a register states at most ${LINE_FILTERS} filters \u2014 keep the ones narrowed by daily` }).optional().describe(
33450
+ filters: lineFilters("a register").optional().describe(
33104
33451
  `The fields the reader narrows the rows by every day, in that order \u2014 each earns its place by that daily use, so none to ${LINE_FILTERS} is normal and ${LINE_FILTERS} is a cap, never a quota (a select, a member, a link, a date, a yes/no, a number as a range, or its \`tiers\` where the business narrows by those bands daily) \u2014 each on a desk's line, in one Filters sheet on a phone. Never the status (its chips); each a field the rows show \u2014 a column, or a part of the row. A threshold the business names ("large orders") is a formula yes/no here; a column's header sorts by a day or an amount`
33105
33452
  ),
33106
33453
  search: zod_default.array(fieldAlias).min(1).optional().describe(
@@ -33118,9 +33465,10 @@ var registerSchema = zod_default.object({
33118
33465
  period: registerPeriodSchema.optional().describe(
33119
33466
  "With `tabs`: the period they open on \u2014 `today`, `week`, `month`, `quarter` or `year` so far, or `{ days }`, the last that many days through now (a night shift running past midnight reads two); absent, this month so far"
33120
33467
  ),
33121
- layout: zod_default.enum(["table", "cards", "calendar", "roster", "lanes"]).optional().describe(
33122
- "`cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); absent, a table"
33468
+ layout: zod_default.enum(["table", "cards", "calendar", "roster", "lanes", "gantt"]).optional().describe(
33469
+ "`cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); `gantt` for rows each planned over a run of days, one bar a row on one axis of time (a shipment from its sailing to its arrival, a hire from its start to its return, a task of a project) \u2014 its dates in `gantt`, its lanes the register's `group`; absent, a table"
33123
33470
  ),
33471
+ gantt: registerGanttSchema.optional().describe('With `layout: "gantt"`: the dates and facts each row\'s bar is drawn from'),
33124
33472
  roster: alias.optional().describe(
33125
33473
  "With `layout: \"roster\"`: the entity whose rows fill the days \u2014 one single link to this entity, and a date on its row line (`records` title or subtitle). A cell reads its row's status, else the first single select on its line, several rows a mark each, the cell read by the one latest in that select's options; a row whose line holds a second date after the first fills every day to it; a day with none stays empty, never an absence"
33126
33474
  ),
@@ -33148,6 +33496,9 @@ var registerSchema = zod_default.object({
33148
33496
  opens: narrowingHolds(
33149
33497
  'The view the register opens on: each entry a narrowing on a field the rows show that stands as a chip the reader may take away (the rows still owed), `"me"` a member field holding the reader (the rows mine). One on the status replaces the chips\' opening on the open work'
33150
33498
  ).optional(),
33499
+ remove: zod_default.literal(false).optional().describe(
33500
+ "The app edits its records and offers no Delete on them (a row's \u22EF, its record's \u22EF) \u2014 a desk correcting a date of records another app makes and closes; absent, a record the app writes is deleted where it stands"
33501
+ ),
33151
33502
  create: zod_default.union([zod_default.array(fieldAlias).min(1), zod_default.literal(false)]).optional().describe("The fields asked when a row is added; absent, the title, the subtitle and figure no default fills, the natural key and every required field \u2014 each a person writes; false, rows are not added here"),
33152
33503
  export: zod_default.union([
33153
33504
  zod_default.literal(true),
@@ -33194,7 +33545,9 @@ var blockSchema = zod_default.union([
33194
33545
  rows: alias.describe("A child entity whose rows belong to this record"),
33195
33546
  via: blockVia,
33196
33547
  title: blockTitle,
33197
- columns: zod_default.array(fieldAlias).optional().describe("The child's fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure's meter reads against), though an `expect` block's figure is a column, filled in place"),
33548
+ columns: zod_default.array(fieldAlias).optional().describe(
33549
+ "The child's fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure's meter reads against), though an `expect` block's figure is a column, filled in place."
33550
+ ),
33198
33551
  create: blockCreateSchema("its title, its picture, the subtitle and figure no default fills, the block's columns, its natural key and every required field"),
33199
33552
  where: selectsHold(
33200
33553
  "Only the child rows whose single select holds one of these options \u2014 every entry ANDed, read so on the server; a row added here holds them (one leg's fees, of a record holding both legs'). Each select is one the child requires, so no row stands in no block"
@@ -33208,6 +33561,18 @@ var blockSchema = zod_default.union([
33208
33561
  under: fieldAlias.optional().describe(
33209
33562
  "The child's one-link to a document that covers its rows (an invoice over its fee lines), whose many-link points back and which links to this record: each document stands as a heading over the lines it covers; a filled line no document covers says so on its own row with a make for one document over it, and two or more waiting take a combined make in the block's heading. Never also a rows block of the document"
33210
33563
  ),
33564
+ filters: lineFilters("a rows block").optional().describe(
33565
+ `The child's fields the reader narrows the block's rows by, as a register's \`filters\` \u2014 none to ${LINE_FILTERS}, each a field its rows show (a column, or a part of the row), its status among them where the business narrows by it \u2014 never on a task list, whose status chips narrow it`
33566
+ ),
33567
+ search: zod_default.array(fieldAlias).min(1).optional().describe("The child's fields the block's search box matches (a link by its row's title), a pasted list line by line; absent, the block has no search box"),
33568
+ group: fieldAlias.optional().describe("The child's field the block's rows open grouped by, as a register's `group` \u2014 the reader's grouping starting there; absent, ungrouped"),
33569
+ sort: sortSchema.optional().describe("The order the block's rows open in, as a register's `sort` \u2014 within each group where the block groups them; absent, the child's first deadline soonest first, else the newest first"),
33570
+ edits: zod_default.array(fieldAlias).min(1).max(LINE_EDITS, { error: `a line writes at most ${LINE_EDITS} fields in place \u2014 a line is read, not a form; the rest are written in the row it opens` }).optional().describe(
33571
+ `The child's fields each line holds as its control, written where the line stands (a quote line's sell price) \u2014 a number, a date, words or one option the row's save writes, each a column or the figure, at most ${LINE_EDITS}; every other value reads, and the row opens to write it`
33572
+ ),
33573
+ remove: zod_default.literal(false).optional().describe(
33574
+ "This list offers no Delete on its rows (its open row and \u22EF); they are deleted where another list of them or their own record offers it \u2014 e.g. lines that are a selection of rows kept elsewhere"
33575
+ ),
33211
33576
  when: shownWhen.optional()
33212
33577
  }).strict(),
33213
33578
  zod_default.object({
@@ -33269,6 +33634,9 @@ var recordSectionSchema = zod_default.object({
33269
33634
  "Acts of this app drawn under the section's fields, their asks as more of them and what each lacks said above its button; the header draws no act, so every act of the record itself is named by one section \u2014 an act of a child (`of`) stands on the child's row and is never named here. An act moving the status, offered at ONE step (the stage its `when` holds), stands at that step's foot when it moves the work forward \u2014 to a later option, an exit too \u2014 or back to a step the work passes anyway; one offered at several steps, or moving back into a detour, stands in the section it enters, which draws it while the work waits \u2014 an exit's in the header's \u22EF, since an exit stands only once the record is in it. A correction \u2014 `danger` and not one of its step's outcomes (no other status move offered at the same moment) \u2014 waits in the section heading's \u22EF, and one no section names in the header's \u22EF; every other act is named by a section"
33270
33635
  ),
33271
33636
  when: shownWhen.optional(),
33637
+ side: zod_default.literal(true).optional().describe(
33638
+ "The section the rest are worked from, drawn in a pane beside them rather than in their scroll \u2014 the photos looked at, the pool picked from, the paper typed off; at most one on a record, which opens on a page door. It holds what any section holds"
33639
+ ),
33272
33640
  page: zod_default.literal(true).optional().describe(
33273
33641
  "A page of its own, off the record's scroll: its own heading and a way back, reached from the record's rail after the scroll's sections, or from a link at its place where the record has no rail \u2014 for what makes the record worse stacked in it: a long list of related rows, a workspace of its own. Only on a page door, never a step (`at`)"
33274
33642
  )
@@ -33276,8 +33644,8 @@ var recordSectionSchema = zod_default.object({
33276
33644
  var RETIRED_RECORD_KEYS = /* @__PURE__ */ new Set(["facts", "blocks"]);
33277
33645
  var recordSchema = zod_default.object(
33278
33646
  {
33279
- door: zod_default.enum(["page", "drawer"]).optional().describe(
33280
- "A page for work read at length; a drawer for rows worked one after another from the register. Either stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page` or the record has more than three sections, else a drawer"
33647
+ door: zod_default.enum(["page", "drawer", "beside"]).optional().describe(
33648
+ "A page for work read at length; a drawer for rows worked one after another from the register; `beside` for rows worked one after another while the list stays in view \u2014 the register's list and the open record side by side, over a table register alone. Each stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page`, the record has more than three sections or one stands beside the rest (`side`), else a drawer"
33281
33649
  ),
33282
33650
  sections: zod_default.array(recordSectionSchema).min(1).optional().describe(
33283
33651
  "The record in the order its work reaches it \u2014 each section its fields, then its blocks, then its acts. Absent, one section of every field a person writes that no other place shows"
@@ -33306,7 +33674,7 @@ var actInputSchema = zod_default.object({
33306
33674
  default: fieldAlias.optional().describe("A field of the row whose value the input starts from, still changed at will")
33307
33675
  }).strict();
33308
33676
  var actTemplateSchema = zod_default.object({
33309
- template: alias.describe("A document template the act makes a paper of, kept in `into`"),
33677
+ template: alias.describe("A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook"),
33310
33678
  when: conditionsHold(
33311
33679
  "The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked"
33312
33680
  ).optional()
@@ -33320,14 +33688,15 @@ var actRecordSchema = zod_default.object({
33320
33688
  by: fieldAlias.optional().describe("The child's member field set to the person who recorded")
33321
33689
  }).strict();
33322
33690
  var FILL_REF = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
33691
+ var setFormulaSchema = zod_default.object({ formula: zod_default.string().min(1).describe("A formula over the acted row's fields, `{alias}` each, read as the entity's own formulas read them") }).strict();
33323
33692
  var actSchema = zod_default.object({
33324
33693
  alias: alias.describe("The act's name within its app \u2014 its workflow is `act_<alias>`"),
33325
33694
  label: zod_default.string().min(1).describe("The verb, as the reader says it"),
33326
33695
  on: zod_default.enum(["record", "rows", "view"]).optional().describe(
33327
- "`rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked in the record's task list; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row's menu)"
33696
+ "`rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked together in a rows block of the record; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row's menu)"
33328
33697
  ),
33329
33698
  of: alias.optional().describe(
33330
- "A rows block's child entity the act works on: offered on each child row \u2014 or, `on: \"rows\"`, on the rows ticked together in a task list \u2014 its conditions and its write that row's"
33699
+ "A rows block's child entity the act works on: offered on each child row \u2014 or, `on: \"rows\"`, on the rows ticked together in the block \u2014 its conditions and its write that row's"
33331
33700
  ),
33332
33701
  when: conditionsHold(
33333
33702
  "The act works only while each named select holds one of these options, and each named yes/no (stored, or a formula) is this value"
@@ -33335,13 +33704,26 @@ var actSchema = zod_default.object({
33335
33704
  requires: zod_default.array(fieldAlias).min(1).optional().describe("Fields that must be filled first, each drawn where the act stands (its section's fields, a block's, or the act's asks \u2014 never the header alone); a blocked act names each one missing"),
33336
33705
  recommends: zod_default.array(fieldAlias).min(1).optional().describe("Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank"),
33337
33706
  asks: zod_default.array(zod_default.union([fieldAlias, actInputSchema])).min(1).optional().describe("What the reader states when pressing it \u2014 fields of the record (written by the act) or inputs"),
33338
- set: zod_default.record(fieldAlias, zod_default.union([zod_default.string(), zod_default.number(), zod_default.boolean(), zod_default.null()])).optional().describe('What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"`'),
33707
+ set: zod_default.record(
33708
+ fieldAlias,
33709
+ zod_default.union([
33710
+ zod_default.string(),
33711
+ zod_default.number(),
33712
+ zod_default.boolean(),
33713
+ zod_default.null(),
33714
+ setFormulaSchema
33715
+ ])
33716
+ ).optional().describe(
33717
+ 'What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"` \u2014 each the act\'s outcome, read-only elsewhere; or `{ "formula": \u2026 }`, computed over each acted row as it stands when the act runs and written then (a sell price at cost and markup): a starting value, which a person may change after and the act never recomputes'
33718
+ ),
33339
33719
  workflow: alias.optional().describe("An authored workflow (src/workflows/<alias>.ts) the act runs after its checks, for what `set` cannot say"),
33340
- template: alias.optional().describe("A document template the act makes a file from \u2014 over several rows, one file of them all, the rows listed under the entity's alias"),
33720
+ template: alias.optional().describe(
33721
+ "A document template the act makes a file from \u2014 an html one made a PDF, an excel one a workbook; over several rows, one file of them all, the rows listed under the entity's alias"
33722
+ ),
33341
33723
  templates: zod_default.array(actTemplateSchema).min(2).optional().describe(
33342
33724
  "Several papers the act makes into `into` (a case's forms), instead of one `template`: drawn where the act stands as a list of its papers \u2014 each made one as its file, each not yet made as a placeholder naming it \u2014 the reader ticks which to make, and one press makes those, each replacing the paper its template made before"
33343
33725
  ),
33344
- into: fieldAlias.optional().describe("The files field of the one record the made document is kept in"),
33726
+ into: fieldAlias.optional().describe("The files field of the one record the made document is kept in \u2014 remade, it replaces the paper its template made before, never a file a person put there"),
33345
33727
  intake: alias.optional().describe(
33346
33728
  "A child the record lists in a rows block whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child \u2014 its kind the child's title, its file in the child's files field"
33347
33729
  ),
@@ -34071,6 +34453,26 @@ function readField(model, entity, field, hops = 0) {
34071
34453
  if (inner === void 0 || link?.type !== "select_record_link") return void 0;
34072
34454
  return { ...inner, several: inner.several || link.cardinality !== "one" };
34073
34455
  }
34456
+ function appliesKind(model, entity, field) {
34457
+ if (isSingleSelect(field)) return "select";
34458
+ return field.type === "boolean" || field.type === "formula" && resolvedFieldType(field, entity, model.entities) === "boolean" ? "yes/no" : void 0;
34459
+ }
34460
+ function appliesCondition(model, entity, alias2) {
34461
+ const field = fieldOf(entity, alias2);
34462
+ if (field === void 0) return { refused: `names "${alias2}", which is not a field of "${entity.alias}"` };
34463
+ if (field.type !== "lookup") {
34464
+ const kind2 = appliesKind(model, entity, field);
34465
+ return kind2 === void 0 ? { refused: `"${alias2}" is a ${field.type} field \u2014 a field applies by a yes/no or a single select` } : { kind: kind2, field };
34466
+ }
34467
+ const link = fieldOf(entity, field.source_field_alias);
34468
+ const target = link?.type === "select_record_link" && link.cardinality === "one" ? entityOf(model, link.target_entity) : void 0;
34469
+ const read = target === void 0 ? void 0 : fieldOf(target, field.lookup_field_alias);
34470
+ const kind = target === void 0 || read === void 0 ? void 0 : appliesKind(model, target, read);
34471
+ if (target === void 0 || read === void 0 || kind === void 0) {
34472
+ return { refused: `"${alias2}" looks up no yes/no or single select through a one-row link \u2014 a field applies by one of the row's own, or one looked up through a link naming one row` };
34473
+ }
34474
+ return { kind, field: read, through: { link: field.source_field_alias, entity: target.alias, field: read.alias } };
34475
+ }
34074
34476
  function linkedEntity(model, entity, field) {
34075
34477
  const read = readField(model, entity, field);
34076
34478
  return read?.field.type === "select_record_link" ? read.field.target_entity : void 0;
@@ -34139,8 +34541,8 @@ function isTaskEntity(model, entity) {
34139
34541
  function isTaskBlock(model, block) {
34140
34542
  return isRowsBlock(block) && block.expect === void 0 && block.under === void 0 && isTaskEntity(model, block.rows);
34141
34543
  }
34142
- function listsTasks(model, app, child) {
34143
- return recordBlocks(app.record).some((block) => isTaskBlock(model, block) && block.rows === child);
34544
+ function listsRows(app, child) {
34545
+ return recordBlocks(app.record).some((block) => isRowsBlock(block) && block.expect === void 0 && block.rows === child);
34144
34546
  }
34145
34547
  function actEntity(app, act) {
34146
34548
  return act.of ?? app.entity;
@@ -34195,11 +34597,14 @@ function actStamps(model, entity) {
34195
34597
  for (const app of registerApps(model)) {
34196
34598
  for (const [act, one] of (app.acts ?? []).entries()) {
34197
34599
  if (actEntity(app, one) !== entity) continue;
34198
- for (const name of Object.keys(one.set ?? {})) if (!stamps.has(name)) stamps.set(name, { app: app.alias, act });
34600
+ for (const [name, value] of Object.entries(one.set ?? {})) if (!isSetFormula(value) && !stamps.has(name)) stamps.set(name, { app: app.alias, act });
34199
34601
  }
34200
34602
  }
34201
34603
  return stamps;
34202
34604
  }
34605
+ function isSetFormula(value) {
34606
+ return value !== null && typeof value === "object";
34607
+ }
34203
34608
  function actSetFields(model, entity) {
34204
34609
  return new Set(actStamps(model, entity).keys());
34205
34610
  }
@@ -34218,7 +34623,7 @@ function unfillable(model, entity, field) {
34218
34623
  if (actOwnedFields(model, entity.alias).has(field.alias)) return "written by an act, the one way it changes";
34219
34624
  return void 0;
34220
34625
  }
34221
- function blockTotals(model, parent, child, via, columns, where) {
34626
+ function blockTotals(model, parent, child, via, columns, where, blocks) {
34222
34627
  const record2 = entityOf(model, parent);
34223
34628
  if (record2 === void 0) return /* @__PURE__ */ new Map();
34224
34629
  const figure = recordDisplay(model, child)?.figure;
@@ -34232,7 +34637,8 @@ function blockTotals(model, parent, child, via, columns, where) {
34232
34637
  (field) => field.type === "rollup" && links.has(field.source_field_alias) && field.aggregate_option.operation === "sum" && field.aggregate_option.field_key === column ? [field] : []
34233
34638
  );
34234
34639
  const narrowedAlike = sums.filter((sum2) => sameSelects(selectsOf(sum2.filter), where));
34235
- const candidates = where.length > 0 ? narrowedAlike : sums.length === 1 ? sums : narrowedAlike;
34640
+ const homed = (sum2) => selectsOf(sum2.filter).length > 0 && blocks.some((other) => other.length > 0 && sameSelects(selectsOf(sum2.filter), other));
34641
+ const candidates = where.length > 0 ? narrowedAlike : sums.length === 1 ? sums.filter((sum2) => !homed(sum2)) : narrowedAlike;
34236
34642
  const [only] = candidates;
34237
34643
  return candidates.length === 1 && only !== void 0 ? [[column, only.alias]] : [];
34238
34644
  })
@@ -34349,6 +34755,7 @@ function historyShape(model, parent) {
34349
34755
  }
34350
34756
  function actSetValue(field, value, inputs) {
34351
34757
  if (!isAsked(field)) return { refused: `writes "${field.alias}", a ${field.type} field no write can set` };
34758
+ if (value !== null && typeof value === "object") return { kind: "formula", expression: value.formula };
34352
34759
  if (value === null) {
34353
34760
  return field.required === true ? { refused: `clears "${field.alias}", which is required` } : { kind: "clear" };
34354
34761
  }
@@ -34435,6 +34842,70 @@ function lineSpan(model, entity) {
34435
34842
  const [start, end] = typedLine(model, entity).filter((one) => one.type === "date");
34436
34843
  return { ...start === void 0 ? {} : { start: start.name }, ...end === void 0 ? {} : { end: end.name } };
34437
34844
  }
34845
+ function ganttShape(model, parent, gantt) {
34846
+ const entity = entityOf(model, parent);
34847
+ if (entity === void 0) return { refused: [{ at: "", reason: `reads "${parent}", which this model does not declare` }] };
34848
+ const refused = [];
34849
+ const refuse = (at2, reason) => {
34850
+ refused.push({ at: at2, reason });
34851
+ };
34852
+ const dated = (name, at2, role) => {
34853
+ const field = fieldOf(entity, name);
34854
+ if (field === void 0) refuse(at2, `names "${name}", which is not a field of "${parent}"`);
34855
+ else if (resolvedFieldType(field, entity, model.entities) !== "date") refuse(at2, `"${name}" is a ${field.type} field \u2014 ${role} is a date`);
34856
+ else return field;
34857
+ return void 0;
34858
+ };
34859
+ const moment = (field) => holdsMoment(field, entity, model.entities);
34860
+ const alike = (field, against, at2, role) => {
34861
+ if (field !== void 0 && against !== void 0 && moment(field) !== moment(against)) {
34862
+ refuse(at2, `"${field.alias}" is ${moment(field) ? "a moment" : "a day"} and "${against.alias}" ${moment(against) ? "a moment" : "a day"} \u2014 ${role} is read as the bar's start is`);
34863
+ }
34864
+ };
34865
+ const start = dated(gantt.start, "start", "a bar's start");
34866
+ const due = model.records?.[parent]?.due?.[0];
34867
+ const endName = gantt.end ?? due;
34868
+ if (endName === void 0) refuse("end", `"${parent}" states no deadline (\`records.${parent}.due\`) \u2014 name the date each bar ends at in \`end\``);
34869
+ const end = endName === void 0 ? void 0 : dated(endName, "end", "a bar's end");
34870
+ if (endName !== void 0 && endName === gantt.start) refuse("end", `ends each bar at "${endName}", the date it starts at \u2014 a bar runs between two dates`);
34871
+ else alike(end, start, "end", "its end");
34872
+ const ends = /* @__PURE__ */ new Set([gantt.start, ...endName === void 0 ? [] : [endName]]);
34873
+ const marked = /* @__PURE__ */ new Set();
34874
+ for (const [index, name] of (gantt.milestones ?? []).entries()) {
34875
+ const at2 = `milestones.${index}`;
34876
+ if (ends.has(name)) refuse(at2, `"${name}" is the bar's own ${name === gantt.start ? "start" : "end"} \u2014 a milestone is another date on its line`);
34877
+ else if (marked.has(name)) refuse(at2, `names "${name}" twice \u2014 a date is one diamond`);
34878
+ else dated(name, at2, "a milestone");
34879
+ marked.add(name);
34880
+ }
34881
+ for (const key of ["start", "end"]) {
34882
+ const name = gantt.planned?.[key];
34883
+ if (name === void 0) continue;
34884
+ alike(dated(name, `planned.${key}`, `a planned ${key}`), start, `planned.${key}`, `a planned ${key}`);
34885
+ }
34886
+ if (gantt.progress !== void 0) {
34887
+ const field = fieldOf(entity, gantt.progress);
34888
+ if (field === void 0) refuse("progress", `names "${gantt.progress}", which is not a field of "${parent}"`);
34889
+ else if (resolvedFieldType(field, entity, model.entities) !== "number" || contractNumberUnit(field, entity, model.entities).percent !== true) {
34890
+ refuse("progress", `"${gantt.progress}" is ${field.type === "number" ? "a number that is no percent" : `a ${field.type} field`} \u2014 how far a row is done is a percent (0\u2013100)`);
34891
+ }
34892
+ }
34893
+ if (gantt.after !== void 0) {
34894
+ const field = fieldOf(entity, gantt.after);
34895
+ if (field === void 0) refuse("after", `names "${gantt.after}", which is not a field of "${parent}"`);
34896
+ else if (field.type !== "select_record_link") refuse("after", `"${gantt.after}" is a ${field.type} field \u2014 the rows a row waits on are a link to "${parent}" rows`);
34897
+ else if (field.target_entity !== parent) refuse("after", `"${gantt.after}" links to "${field.target_entity}" \u2014 a row waits on rows of its own entity, "${parent}"`);
34898
+ }
34899
+ if (refused.length > 0 || endName === void 0) return { refused };
34900
+ return {
34901
+ start: gantt.start,
34902
+ end: endName,
34903
+ milestones: gantt.milestones ?? [],
34904
+ ...gantt.planned === void 0 ? {} : { planned: { ...gantt.planned } },
34905
+ ...gantt.progress === void 0 ? {} : { progress: gantt.progress },
34906
+ ...gantt.after === void 0 ? {} : { after: gantt.after }
34907
+ };
34908
+ }
34438
34909
  function rosterShape(model, parent, alias2, dated = true) {
34439
34910
  const child = entityOf(model, alias2);
34440
34911
  if (child === void 0) return { refused: `names "${alias2}", which this model does not declare` };
@@ -34639,19 +35110,18 @@ function checkNarrowing(entities, entity, narrowing, at2) {
34639
35110
  const conditions = Object.fromEntries(entries2.filter((entry) => entry[1] !== "me"));
34640
35111
  return [...mine, ...checkWhen(entities, entity, conditions, at2, "offer")];
34641
35112
  }
34642
- var FILLS = { act: ["html"], export: ["html", "excel"] };
34643
- function checkTemplate(model, template, path15, by) {
35113
+ var MADE_FROM = ["html", "excel"];
35114
+ function checkTemplate(model, template, path15) {
34644
35115
  if (model.templates === void 0) return [];
34645
35116
  const declared = model.templates.find((one) => one.alias === template);
34646
35117
  if (declared === void 0) return [error51("model.names-declared", path15, `names template "${template}", which this model does not declare`)];
34647
- if (!FILLS[by].includes(declared.type)) {
34648
- const fills = by === "act" ? "an act makes its document from an html one" : "an export fills an html or an excel one";
34649
- return [error51("model.template-kind", path15, `names the ${declared.type} template "${template}" \u2014 ${fills}`)];
35118
+ if (!MADE_FROM.includes(declared.type)) {
35119
+ return [error51("model.template-kind", path15, `names the ${declared.type} template "${template}" \u2014 a document is made from an html or an excel one`)];
34650
35120
  }
34651
35121
  return [];
34652
35122
  }
34653
35123
  function checkExportTemplate(model, entity, tabs, template, path15) {
34654
- const kind = checkTemplate(model, template, path15, "export");
35124
+ const kind = checkTemplate(model, template, path15);
34655
35125
  if (kind.length > 0 || model.templates === void 0) return kind;
34656
35126
  const reports = APP_EXPORT_REPORT_KEYS;
34657
35127
  if (reports.includes(entity)) {
@@ -34870,6 +35340,28 @@ function checkAppRow(model, app, at2) {
34870
35340
  }
34871
35341
  return findings;
34872
35342
  }
35343
+ function checkApplies(model, entity, display, at2) {
35344
+ return Object.entries(display.applies ?? {}).flatMap(([name, conditions]) => {
35345
+ const path15 = `${at2}.applies.${name}`;
35346
+ const applying = fieldOf(entity, name);
35347
+ if (applying === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
35348
+ if (name === display.title) return [error51("records.applies", path15, `"${name}" is the row's title, which names it wherever it is drawn \u2014 it always applies`)];
35349
+ const own = applying.required === true && !hasDefault(applying) ? [error51("records.applies", path15, `"${name}" is required and starts at no \`default\` \u2014 where it does not apply no add asks it, so a row could not be made; give it a default, or make it optional`)] : [];
35350
+ const held = Object.entries(conditions).flatMap(([condition, value]) => {
35351
+ const at3 = `${path15}.${condition}`;
35352
+ if (condition === name) return [error51("records.applies", at3, `"${name}" applies by itself \u2014 a field applies by another of the row's`)];
35353
+ const shape = appliesCondition(model, entity, condition);
35354
+ if ("refused" in shape) return [error51(shape.refused.startsWith("names") ? "model.names-declared" : "records.applies", at3, shape.refused)];
35355
+ if (typeof value === "boolean") {
35356
+ return shape.kind === "yes/no" ? [] : [error51("records.applies", at3, `"${condition}" is a single select \u2014 it applies by its options, never \`${String(value)}\``)];
35357
+ }
35358
+ if (shape.kind !== "select" || shape.field.type !== "select") return [error51("records.applies", at3, `"${condition}" is a yes/no \u2014 it applies by \`true\` or \`false\``)];
35359
+ const declared = new Set(shape.field.options.map((option) => option.alias));
35360
+ return value.flatMap((option, index) => declared.has(option) ? [] : [error51("model.names-declared", `${at3}.${index}`, `names "${option}", which is not an option of "${shape.field.alias}"`)]);
35361
+ });
35362
+ return [...own, ...held];
35363
+ });
35364
+ }
34873
35365
  function checkRecords(model) {
34874
35366
  const findings = [];
34875
35367
  for (const [alias2, display] of Object.entries(model.records ?? {})) {
@@ -34890,6 +35382,7 @@ function checkRecords(model) {
34890
35382
  findings.push(error51("records.title", `${at2}.title`, `"${title.alias}" is a code \u2014 a row is named by what it is (a name, a party); put the code in \`subtitle\``));
34891
35383
  }
34892
35384
  for (const [index, name] of (display.subtitle ?? []).entries()) field(name, `${at2}.subtitle.${index}`);
35385
+ findings.push(...checkApplies(model, entity, display, at2));
34893
35386
  if (display.image !== void 0) {
34894
35387
  const image = field(display.image, `${at2}.image`);
34895
35388
  if (image !== void 0 && image.type !== "files") findings.push(error51("records.image", `${at2}.image`, `"${image.alias}" is a ${image.type} field \u2014 a row is pictured by a files field`));
@@ -35089,68 +35582,350 @@ function readingFields(reading) {
35089
35582
  }
35090
35583
  }
35091
35584
 
35092
- // ../shared/src/schemas/app_model_plan.ts
35093
- function recordHeaderFields(display) {
35094
- if (display === void 0) return [];
35095
- return [
35096
- display.title,
35097
- ...display.subtitle ?? [],
35098
- ...display.image === void 0 ? [] : [display.image],
35099
- ...display.status?.field === void 0 ? [] : [display.status.field],
35100
- ...(milestonesOf(display) ?? []).map((stage) => stage.field),
35101
- ...display.figure === void 0 ? [] : [display.figure],
35102
- ...display.description === void 0 ? [] : [display.description]
35103
- ];
35104
- }
35105
- function historyOf(model, parent) {
35106
- const shape = historyShape(model, parent);
35107
- return shape === void 0 || "refused" in shape ? void 0 : shape;
35108
- }
35109
- function defaultCreate(model, entity, excluded) {
35110
- return createAsking(model, entity, excluded, []);
35585
+ // ../shared/src/schemas/write_rules.ts
35586
+ var BOUND_REF = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
35587
+ var BOUND_REF_MESSAGE = 'a bound is a number, "<field alias>" or "<link alias>.<field alias>"';
35588
+ var SUGGEST_REF = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
35589
+ function suggestRef(stated) {
35590
+ const [entity = "", field = ""] = stated.split(".");
35591
+ return { entity, field };
35111
35592
  }
35112
- function defaultBlockCreate(model, child, excluded, columns) {
35113
- const image = model.records?.[child.alias]?.image;
35114
- return createAsking(model, child, excluded, [...image === void 0 ? [] : [image], ...columns]);
35593
+ var fieldWriteRuleSchema = zod_default.object({
35594
+ default_from: zod_default.string().regex(/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/, 'default_from is "<link alias>.<field alias>"').optional().describe(
35595
+ "Copy this field's value from the linked row at CREATE time \u2014 `<link alias>.<field alias>`, the link being a one-row link on this entity, required or filled by the add (the record a row is added under). The value is copied rather than looked up, so the source changing later leaves the row alone."
35596
+ ),
35597
+ min: zod_default.union([zod_default.number(), zod_default.string().regex(BOUND_REF, BOUND_REF_MESSAGE)]).optional().describe(
35598
+ "Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line's day inside its trip). On a figure whose unit or currency is per row (`unit_field`, `currency_field`) a constant bound is only 0; bound it by a field of the row"
35599
+ ),
35600
+ max: zod_default.union([zod_default.number(), zod_default.string().regex(BOUND_REF, BOUND_REF_MESSAGE)]).optional().describe("Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names"),
35601
+ options_where: tableRecordFiltersSchema.optional().describe(
35602
+ "Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity's own fields, each `field_key` a field alias. It narrows the picker's read and is refused again where the write lands."
35603
+ ),
35604
+ same: zod_default.array(contractAliasSchema).min(1).optional().describe(
35605
+ "One-row links this row and each row this link points at must name alike \u2014 a fee's invoice is one of its own visit's, a box's seal one of its own shipping line's: each a one-row link of both entities to the same entity. It narrows the picker's read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare."
35606
+ ),
35607
+ suggest: zod_default.string().regex(SUGGEST_REF, 'suggest is "<entity alias>.<field alias>"').optional().describe(
35608
+ "A text field whose control offers, as the reader types, the distinct values a text field of a catalog holds \u2014 `<entity alias>.<field alias>` (a damage position's code among the codes on file); any text is still written"
35609
+ )
35610
+ }).strict();
35611
+ var entityWriteRulesSchema = zod_default.object({
35612
+ natural_key: zod_default.array(contractAliasSchema).min(1).optional().describe(
35613
+ "The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches."
35614
+ ),
35615
+ fields: zod_default.record(contractAliasSchema, fieldWriteRuleSchema).optional().describe("Field alias \u2192 what that field's value is copied from, bounded by or picked among"),
35616
+ no_overlap: zod_default.object({
35617
+ from: contractAliasSchema.describe("The stored date (or datetime) a row's span starts on"),
35618
+ to: contractAliasSchema.describe(
35619
+ "The stored date (or datetime) a row's span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at"
35620
+ ),
35621
+ by: zod_default.array(contractAliasSchema).min(1).optional().describe("Fields two rows share to compete for a span (the same employee, the same room); absent, every row"),
35622
+ while: zod_default.record(contractAliasSchema, zod_default.array(contractAliasSchema).min(1)).optional().describe("Only rows at these options of these selects hold their span (running, not closed); absent, every row")
35623
+ }).strict().optional().describe("No two rows hold overlapping spans \u2014 a create or update that would is refused")
35624
+ }).strict();
35625
+ var writeRulesSchema = zod_default.record(contractAliasSchema, entityWriteRulesSchema);
35626
+ function boundRef(bound) {
35627
+ if (typeof bound !== "string") return void 0;
35628
+ const [first2 = "", second] = bound.split(".");
35629
+ return second === void 0 ? { field: first2 } : { link: first2, field: second };
35115
35630
  }
35116
- function createAsking(model, entity, excluded, read) {
35117
- const display = recordDisplay(model, entity.alias);
35118
- const title = display?.title;
35119
- const later = new Set((milestonesOf(display) ?? []).slice(1).map((stage) => stage.field));
35120
- const rules = model.write_rules?.[entity.alias];
35121
- const copied = copiedOnCreate(model, entity, excluded);
35122
- const stamped = actSetFields(model, entity.alias);
35123
- const asked = (field) => field !== void 0 && isAsked(field) && !excluded.includes(field.alias) && !copied.has(field.alias) && !stamped.has(field.alias);
35124
- const withUnit = (alias2) => {
35125
- const field = fieldOf(entity, alias2);
35126
- const select = field === void 0 ? void 0 : contractFigureSelect(field, entity, model.entities);
35127
- const held = select === void 0 ? void 0 : fieldOf(entity, select.alias);
35128
- return asked(held) ? [alias2, held.alias] : [alias2];
35129
- };
35130
- const titleField = title === void 0 ? void 0 : fieldOf(entity, title);
35131
- const figure = display?.figure;
35132
- const bound = figure === void 0 ? void 0 : display?.limits?.[figure];
35133
- const undefaulted = (name) => {
35134
- const field = fieldOf(entity, name);
35135
- return field === void 0 || !hasDefault(field);
35136
- };
35137
- const line = (display?.subtitle ?? []).filter(undefaulted);
35138
- const measured = [...figure === void 0 ? [] : [figure], ...typeof bound === "string" ? [bound] : []].filter(undefaulted);
35139
- const shown = [...line, ...read, ...measured].filter((name) => !later.has(name)).map((name) => fieldOf(entity, name)).filter(asked);
35140
- const keyed = (rules?.natural_key ?? []).map((key) => fieldOf(entity, key)).filter(asked);
35141
- return unique2(
35142
- [
35143
- ...asked(titleField) ? [titleField.alias] : [],
35144
- ...shown.map((field) => field.alias),
35145
- ...keyed.map((field) => field.alias),
35146
- ...requiredOnCreate(model, entity, [...excluded, ...stamped])
35147
- ].flatMap(withUnit)
35148
- );
35631
+ var BOUNDED_TYPES = ["number", "date"];
35632
+ var NATURAL_KEY_TYPES = ["text", "number", "select_record_link"];
35633
+ var OPTIONS_WHERE_OPERATORS = {
35634
+ number: ["equals", "not_equals", "greater_than", "less_than", "greater_than_or_equal_to", "less_than_or_equal_to"],
35635
+ text: ["equals", "not_equals"],
35636
+ boolean: ["equals"],
35637
+ select: ["has_any_of", "has_none_of"]
35638
+ };
35639
+ function optionsWhereConditions(filter2, target) {
35640
+ if (filter2.node_type !== "group" || filter2.logic !== "and") return void 0;
35641
+ const out = [];
35642
+ for (const child of filter2.children) {
35643
+ if (child.node_type !== "condition") return void 0;
35644
+ const field = target.fields.find((one) => one.alias === child.field_key);
35645
+ if (field === void 0) return void 0;
35646
+ const allowed = OPTIONS_WHERE_OPERATORS[child.type ?? ""];
35647
+ if (allowed === void 0 || !allowed.includes(child.operator)) return void 0;
35648
+ out.push({ type: child.type ?? "", field, operator: child.operator, value: child.value });
35649
+ }
35650
+ return out.length === 0 ? void 0 : out;
35149
35651
  }
35150
- function ownAsks(app) {
35151
- return app.acts.flatMap((act) => act.of === void 0 ? act.asks.filter((ask) => typeof ask === "string") : []);
35652
+ function checkBoundRef(entity, field, ref, byAlias, path15) {
35653
+ const entities = [...byAlias.values()];
35654
+ let on = entity;
35655
+ if ("link" in ref) {
35656
+ const link = entity.fields.find((one) => one.alias === ref.link);
35657
+ if (link === void 0 || link.type !== "select_record_link" || link.cardinality !== "one") {
35658
+ return [error51("write.bounds", path15, `reads "${ref.link}", which is not a one-row link of ${entity.label} \u2014 a bound is read off this row or the one row a link names`)];
35659
+ }
35660
+ const target = byAlias.get(link.target_entity);
35661
+ if (target === void 0) return [error51("model.names-declared", path15, `reads "${ref.link}", whose entity "${link.target_entity}" this model does not declare`)];
35662
+ on = target;
35663
+ }
35664
+ const source = on.fields.find((one) => one.alias === ref.field);
35665
+ if (source === void 0) return [error51("model.names-declared", path15, `names "${ref.field}", which is not a field of ${on.label}`)];
35666
+ if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds", path15, `bounds ${field.label} by itself`)];
35667
+ const from = resolvedFieldType(source, on, entities);
35668
+ const into = resolvedFieldType(field, entity, entities);
35669
+ if (from !== into) return [error51("write.bounds", path15, `bounds ${field.label} (${String(into)}) by "${source.label}" (${String(from)}) \u2014 a bound is of the value's own type`)];
35670
+ const apart = boundUnitRefusal(field, source, entity, entities, "link" in ref ? { link: ref.link, on } : void 0);
35671
+ return apart === void 0 ? [] : [error51("write.bounds", path15, apart)];
35152
35672
  }
35153
- function appWritePlan(given, app) {
35673
+ function checkSame(entity, field, same, byAlias, path15) {
35674
+ if (field.type !== "select_record_link") {
35675
+ return [error51("write.same", path15, `${field.label} is a ${field.type} field \u2014 \`same\` narrows the rows a LINK points at`)];
35676
+ }
35677
+ const target = byAlias.get(field.target_entity);
35678
+ if (target === void 0) return [];
35679
+ const oneLink = (on, alias2) => on.fields.find((one) => one.alias === alias2 && one.type === "select_record_link" && one.cardinality === "one");
35680
+ return same.flatMap((alias2, index) => {
35681
+ const own = oneLink(entity, alias2);
35682
+ const theirs = oneLink(target, alias2);
35683
+ const at2 = `${path15}.${index}`;
35684
+ if (own === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${entity.label}`)];
35685
+ if (theirs === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${target.label} \u2014 the row ${field.label} points at names nothing to compare`)];
35686
+ if (own.type !== "select_record_link" || theirs.type !== "select_record_link" || own.target_entity !== theirs.target_entity) {
35687
+ return [error51("write.same", at2, `"${alias2}" names rows of different entities on ${entity.label} and ${target.label}`)];
35688
+ }
35689
+ return [];
35690
+ });
35691
+ }
35692
+ function checkSuggest(entity, field, stated, byAlias, path15) {
35693
+ if (field.type !== "text") return [error51("write.suggest", path15, `${field.label} is a ${field.type} field \u2014 \`suggest\` offers values to a text field a person types`)];
35694
+ const ref = suggestRef(stated);
35695
+ const catalog = byAlias.get(ref.entity);
35696
+ if (catalog === void 0) return [error51("model.names-declared", path15, `names entity "${ref.entity}", which this model does not declare`)];
35697
+ const source = catalog.fields.find((one) => one.alias === ref.field);
35698
+ if (source === void 0) return [error51("model.names-declared", path15, `names "${ref.field}", which is not a field of ${catalog.label}`)];
35699
+ if (source.type !== "text") return [error51("write.suggest", path15, `offers the values of "${source.label}", a ${source.type} field of ${catalog.label} \u2014 the values offered are a stored text field's`)];
35700
+ return [];
35701
+ }
35702
+ function checkNoOverlap(entity, rule, at2) {
35703
+ const findings = [];
35704
+ const own = (name) => entity.fields.find((one) => one.alias === name);
35705
+ for (const side of ["from", "to"]) {
35706
+ const date5 = own(rule[side]);
35707
+ if (date5?.type !== "date") findings.push(error51("write.no-overlap", `${at2}.${side}`, `"${rule[side]}" is not a date field of ${entity.label} \u2014 a span runs between two dates`));
35708
+ }
35709
+ for (const [index, name] of (rule.by ?? []).entries()) {
35710
+ const by = own(name);
35711
+ if (by === void 0 || !["select_record_link", "select_member", "select", "text", "number"].includes(by.type) || "multi" in by && by.multi === true) {
35712
+ findings.push(error51("write.no-overlap", `${at2}.by.${index}`, `"${name}" is not one value rows can share \u2014 a link, a person, one option, a text or a number`));
35713
+ }
35714
+ }
35715
+ for (const [name, options] of Object.entries(rule.while ?? {})) {
35716
+ const select = own(name);
35717
+ if (select?.type !== "select" || select.multi === true) {
35718
+ findings.push(error51("write.no-overlap", `${at2}.while.${name}`, `"${name}" is not a single select of ${entity.label}`));
35719
+ continue;
35720
+ }
35721
+ for (const option of options) {
35722
+ if (!select.options.some((one) => one.alias === option)) findings.push(error51("model.names-declared", `${at2}.while.${name}`, `names option "${option}", which "${select.label}" does not declare`));
35723
+ }
35724
+ }
35725
+ return findings;
35726
+ }
35727
+ function checkWriteRules(entities, rules) {
35728
+ const findings = [];
35729
+ const byAlias = new Map(entities.map((entity) => [entity.alias, entity]));
35730
+ for (const [alias2, entry] of Object.entries(rules)) {
35731
+ const at2 = `write_rules.${alias2}`;
35732
+ const entity = byAlias.get(alias2);
35733
+ if (entity === void 0) {
35734
+ findings.push(error51("model.names-declared", at2, `names entity "${alias2}", which this model does not declare \u2014 write rules are keyed by entity alias (${byAlias.size > 0 ? [...byAlias.keys()].join(", ") : "this model declares none"})`));
35735
+ continue;
35736
+ }
35737
+ const fieldByAlias = new Map(entity.fields.map((field) => [field.alias, field]));
35738
+ if (entry.no_overlap !== void 0) findings.push(...checkNoOverlap(entity, entry.no_overlap, `${at2}.no_overlap`));
35739
+ const keyed = new Set(entry.natural_key ?? []);
35740
+ const uniqueAsSet = (entity.unique ?? []).some((set2) => set2.every((name) => keyed.has(name)));
35741
+ for (const [index, key] of (entry.natural_key ?? []).entries()) {
35742
+ const field = fieldByAlias.get(key);
35743
+ if (field === void 0) {
35744
+ findings.push(error51("model.names-declared", `${at2}.natural_key.${index}`, `names "${key}", which is not a field of ${entity.label}`));
35745
+ continue;
35746
+ }
35747
+ if (!NATURAL_KEY_TYPES.includes(field.type)) {
35748
+ findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} is a ${field.type} field \u2014 a row is recognised by a value a person types back or the one row it belongs with, so a natural key is text, a number or a one-row link`));
35749
+ continue;
35750
+ }
35751
+ if (field.type === "select_record_link" && field.cardinality !== "one") {
35752
+ findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} holds MANY rows \u2014 a row is recognised by the one row it belongs with, so a link in a natural key is of cardinality "one"`));
35753
+ continue;
35754
+ }
35755
+ if (field.type === "text" && field.unique !== true && !uniqueAsSet) {
35756
+ findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} recognises a row and nothing makes it unique \u2014 declare \`unique: true\` on it, or the key's fields as a set in the entity's \`unique\`; two rows sharing it would make find-or-create pick whichever the read answered first`));
35757
+ }
35758
+ }
35759
+ for (const [fieldAlias2, rule] of Object.entries(entry.fields ?? {})) {
35760
+ const path15 = `${at2}.fields.${fieldAlias2}`;
35761
+ const field = fieldByAlias.get(fieldAlias2);
35762
+ if (field === void 0) {
35763
+ findings.push(error51("model.names-declared", path15, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
35764
+ continue;
35765
+ }
35766
+ if (rule.default_from !== void 0) {
35767
+ findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path15}.default_from`));
35768
+ }
35769
+ const bounded = resolvedFieldType(field, entity, entities);
35770
+ if ((rule.min !== void 0 || rule.max !== void 0) && !BOUNDED_TYPES.includes(bounded ?? field.type)) {
35771
+ findings.push(error51("write.bounds", path15, `${field.label} is a ${field.type} field \u2014 \`min\`/\`max\` bound a number or a date`));
35772
+ }
35773
+ for (const side of ["min", "max"]) {
35774
+ const bound = rule[side];
35775
+ if (typeof bound === "number" && bounded === "date") {
35776
+ findings.push(error51("write.bounds", `${path15}.${side}`, `bounds the date ${field.label} by a number \u2014 a date is bounded by a date field`));
35777
+ }
35778
+ const perRow = typeof bound === "number" && bound !== 0 ? contractUnitSource(field, entity, entities) : void 0;
35779
+ if (perRow !== void 0) {
35780
+ findings.push(error51("write.bound-per-row-unit", `${path15}.${side}`, `bounds ${field.label} by ${String(bound)}, which is in no ${perRow.vocabulary}, and each row reads it in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds \u2014 only 0 is the same in every ${perRow.vocabulary}`));
35781
+ }
35782
+ const ref = boundRef(bound);
35783
+ if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path15}.${side}`));
35784
+ }
35785
+ if (typeof rule.min === "number" && typeof rule.max === "number" && rule.min > rule.max) {
35786
+ findings.push(error51("write.bounds", `${path15}.min`, `is above \`max\` (${rule.min} > ${rule.max}) \u2014 the bound refuses every figure`));
35787
+ }
35788
+ if (rule.same !== void 0) findings.push(...checkSame(entity, field, rule.same, byAlias, `${path15}.same`));
35789
+ if (rule.suggest !== void 0) findings.push(...checkSuggest(entity, field, rule.suggest, byAlias, `${path15}.suggest`));
35790
+ if (rule.options_where === void 0) continue;
35791
+ if (field.type !== "select_record_link") {
35792
+ findings.push(error51("write.options-where", `${path15}.options_where`, `${field.label} is a ${field.type} field \u2014 \`options_where\` narrows the rows a LINK may point at`));
35793
+ continue;
35794
+ }
35795
+ const target = byAlias.get(field.target_entity);
35796
+ if (target === void 0) continue;
35797
+ const conditions = optionsWhereConditions(rule.options_where, target);
35798
+ if (conditions === void 0) {
35799
+ findings.push(error51("write.options-where", `${path15}.options_where`, `is not a narrowing this clause can both filter a picker by and refuse a write with: an \`and\` group of plain conditions over ${target.label}'s own fields, each \`field_key\` a field alias and each operator one of ${Object.entries(OPTIONS_WHERE_OPERATORS).map(([type, ops]) => `${type} ${ops.join("/")}`).join("; ")}`));
35800
+ continue;
35801
+ }
35802
+ for (const condition of conditions) {
35803
+ const at3 = `${path15}.options_where`;
35804
+ const perRow = condition.type === "number" ? contractUnitSource(condition.field, target, entities) : void 0;
35805
+ if (perRow !== void 0) {
35806
+ findings.push(error51("write.options-where", at3, `compares "${condition.field.label}", which each row reads in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds, with a bare number \u2014 a narrowing compares a figure read in one ${perRow.vocabulary} for every row`));
35807
+ }
35808
+ if (condition.type !== "select") continue;
35809
+ if (condition.field.type !== "select") {
35810
+ findings.push(error51("write.options-where", at3, `compares "${condition.field.label}" as a select \u2014 it is a ${condition.field.type} field`));
35811
+ continue;
35812
+ }
35813
+ if (!Array.isArray(condition.value)) {
35814
+ findings.push(error51("write.options-where", at3, `narrows "${condition.field.label}" by no option at all \u2014 a select narrowing names the options it admits`));
35815
+ continue;
35816
+ }
35817
+ const declared = new Set(condition.field.options.map((option) => option.alias));
35818
+ for (const named of condition.value) {
35819
+ if (typeof named === "string" && declared.has(named)) continue;
35820
+ findings.push(error51("model.names-declared", at3, `names option "${String(named)}", which "${condition.field.label}" does not declare (${[...declared].join(", ")})`));
35821
+ }
35822
+ }
35823
+ }
35824
+ }
35825
+ return findings;
35826
+ }
35827
+ function checkDefaultFrom(entity, field, stated, byAlias, path15) {
35828
+ const entities = [...byAlias.values()];
35829
+ const [linkAlias, sourceAlias] = stated.split(".");
35830
+ const link = entity.fields.find((one) => one.alias === linkAlias);
35831
+ if (link === void 0 || link.type !== "select_record_link") {
35832
+ return [
35833
+ error51("write.default-from", path15, `names "${linkAlias}", which is not a link field of ${entity.label} \u2014 a default is copied from a row this one points at`)
35834
+ ];
35835
+ }
35836
+ if (link.cardinality !== "one") {
35837
+ return [
35838
+ error51("write.default-from", path15, `reads "${link.label}", which holds MANY rows \u2014 a default copied from a list would take whichever row came back first`)
35839
+ ];
35840
+ }
35841
+ const target = byAlias.get(link.target_entity);
35842
+ const source = target?.fields.find((one) => one.alias === sourceAlias);
35843
+ if (target === void 0 || source === void 0) {
35844
+ return [
35845
+ error51("model.names-declared", path15, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
35846
+ ];
35847
+ }
35848
+ const from = resolvedFieldType(source, target, entities);
35849
+ const into = resolvedFieldType(field, entity, entities);
35850
+ if (from !== into) {
35851
+ return [
35852
+ error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
35853
+ ];
35854
+ }
35855
+ if (source.type !== "select" || field.type !== "select") return [];
35856
+ const findings = [];
35857
+ if (source.multi === true && field.multi !== true) {
35858
+ findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}", which holds several options, into ${field.label}, which holds one \u2014 a copy would keep whichever came first`));
35859
+ }
35860
+ const missing = source.options.filter((option) => !field.options.some((one) => one.alias === option.alias));
35861
+ if (missing.length > 0) {
35862
+ findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" into ${field.label}, which declares no option ${missing.map((option) => `"${option.alias}"`).join(", ")} \u2014 a copied option is written as the one of the same alias`));
35863
+ }
35864
+ return findings;
35865
+ }
35866
+
35867
+ // ../shared/src/schemas/app_model_plan.ts
35868
+ function recordHeaderFields(display) {
35869
+ if (display === void 0) return [];
35870
+ return [
35871
+ display.title,
35872
+ ...display.subtitle ?? [],
35873
+ ...display.image === void 0 ? [] : [display.image],
35874
+ ...display.status?.field === void 0 ? [] : [display.status.field],
35875
+ ...(milestonesOf(display) ?? []).map((stage) => stage.field),
35876
+ ...display.figure === void 0 ? [] : [display.figure],
35877
+ ...display.description === void 0 ? [] : [display.description]
35878
+ ];
35879
+ }
35880
+ function historyOf(model, parent) {
35881
+ const shape = historyShape(model, parent);
35882
+ return shape === void 0 || "refused" in shape ? void 0 : shape;
35883
+ }
35884
+ function defaultCreate(model, entity, excluded) {
35885
+ return createAsking(model, entity, excluded, []);
35886
+ }
35887
+ function defaultBlockCreate(model, child, excluded, columns) {
35888
+ const image = model.records?.[child.alias]?.image;
35889
+ return createAsking(model, child, excluded, [...image === void 0 ? [] : [image], ...columns]);
35890
+ }
35891
+ function createAsking(model, entity, excluded, read) {
35892
+ const display = recordDisplay(model, entity.alias);
35893
+ const title = display?.title;
35894
+ const later = new Set((milestonesOf(display) ?? []).slice(1).map((stage) => stage.field));
35895
+ const rules = model.write_rules?.[entity.alias];
35896
+ const copied = copiedOnCreate(model, entity, excluded);
35897
+ const stamped = actSetFields(model, entity.alias);
35898
+ const asked = (field) => field !== void 0 && isAsked(field) && !excluded.includes(field.alias) && !copied.has(field.alias) && !stamped.has(field.alias);
35899
+ const withUnit = (alias2) => {
35900
+ const field = fieldOf(entity, alias2);
35901
+ const select = field === void 0 ? void 0 : contractFigureSelect(field, entity, model.entities);
35902
+ const held = select === void 0 ? void 0 : fieldOf(entity, select.alias);
35903
+ return asked(held) ? [alias2, held.alias] : [alias2];
35904
+ };
35905
+ const titleField = title === void 0 ? void 0 : fieldOf(entity, title);
35906
+ const figure = display?.figure;
35907
+ const bound = figure === void 0 ? void 0 : display?.limits?.[figure];
35908
+ const undefaulted = (name) => {
35909
+ const field = fieldOf(entity, name);
35910
+ return field === void 0 || !hasDefault(field);
35911
+ };
35912
+ const line = (display?.subtitle ?? []).filter(undefaulted);
35913
+ const measured = [...figure === void 0 ? [] : [figure], ...typeof bound === "string" ? [bound] : []].filter(undefaulted);
35914
+ const shown = [...line, ...read, ...measured].filter((name) => !later.has(name)).map((name) => fieldOf(entity, name)).filter(asked);
35915
+ const keyed = (rules?.natural_key ?? []).map((key) => fieldOf(entity, key)).filter(asked);
35916
+ return unique2(
35917
+ [
35918
+ ...asked(titleField) ? [titleField.alias] : [],
35919
+ ...shown.map((field) => field.alias),
35920
+ ...keyed.map((field) => field.alias),
35921
+ ...requiredOnCreate(model, entity, [...excluded, ...stamped])
35922
+ ].flatMap(withUnit)
35923
+ );
35924
+ }
35925
+ function ownAsks(app) {
35926
+ return app.acts.flatMap((act) => act.of === void 0 ? act.asks.filter((ask) => typeof ask === "string") : []);
35927
+ }
35928
+ function appWritePlan(given, app) {
35154
35929
  const model = withAppRow(given, app);
35155
35930
  const plan = /* @__PURE__ */ new Map();
35156
35931
  const entity = entityOf(model, app.entity);
@@ -35167,6 +35942,8 @@ function appWritePlan(given, app) {
35167
35942
  ...recordHeaderFields(display),
35168
35943
  // A lanes board moves a row to another lane where it stands.
35169
35944
  ...app.register.lanes === void 0 ? [] : [app.register.lanes.field],
35945
+ // A gantt moves a bar and stretches its end where it stands.
35946
+ ...app.register.gantt === void 0 ? [] : [app.register.gantt.start, app.register.gantt.end],
35170
35947
  ...app.record.sections.flatMap((section) => section.fields),
35171
35948
  ...resolvedBlocks(app.record).flatMap((block) => block.kind === "files" ? block.fields : block.kind === "text" ? [block.field] : [])
35172
35949
  ]);
@@ -35176,7 +35953,8 @@ function appWritePlan(given, app) {
35176
35953
  ...app.register.create === void 0 ? {} : { create: added(entity.alias, app.register.create), ...app.scope === void 0 ? {} : { parent: app.scope.field } },
35177
35954
  stamps: [],
35178
35955
  update: saves(entity, fields),
35179
- remove: true
35956
+ // A register saying it offers no Delete leaves its records to a list that does, or to none.
35957
+ remove: app.register.remove !== false
35180
35958
  });
35181
35959
  }
35182
35960
  const saved = (child, fixed) => {
@@ -35203,10 +35981,16 @@ function appWritePlan(given, app) {
35203
35981
  plan.set(document.alias, { ...under.create === void 0 ? {} : { create: added(document.alias, under.create, fixed), parent: under.via }, stamps: [], update: saved(document, fixed), remove: true });
35204
35982
  }
35205
35983
  const child = entityOf(model, block.entity);
35206
- if (child === void 0 || !peopleWrite(child) || plan.has(child.alias)) continue;
35984
+ if (child === void 0 || !peopleWrite(child)) continue;
35985
+ const removes = block.kind !== "rows" || block.remove !== false;
35986
+ const held = plan.get(child.alias);
35987
+ if (held !== void 0) {
35988
+ if (removes && !held.remove) plan.set(child.alias, { ...held, remove: true });
35989
+ continue;
35990
+ }
35207
35991
  const update = saved(child, [block.via, ...under === void 0 ? [] : [under.field]]);
35208
35992
  if (block.kind === "rows" || block.kind === "agenda") {
35209
- plan.set(child.alias, { ...block.create === void 0 ? {} : { create: added(child.alias, block.create), parent: block.via }, stamps: [], update, remove: true });
35993
+ plan.set(child.alias, { ...block.create === void 0 ? {} : { create: added(child.alias, block.create), parent: block.via }, stamps: [], update, remove: removes });
35210
35994
  continue;
35211
35995
  }
35212
35996
  const asked = new Set(block.create ?? []);
@@ -35273,6 +36057,17 @@ function actFields(act) {
35273
36057
  ...act.intake === void 0 ? [] : [...act.intake.fields, ...act.intake.parties.map((party) => party.link)]
35274
36058
  ];
35275
36059
  }
36060
+ function ganttFields(gantt) {
36061
+ return [
36062
+ gantt.start,
36063
+ gantt.end,
36064
+ ...gantt.milestones,
36065
+ ...gantt.planned?.start === void 0 ? [] : [gantt.planned.start],
36066
+ ...gantt.planned?.end === void 0 ? [] : [gantt.planned.end],
36067
+ ...gantt.progress === void 0 ? [] : [gantt.progress],
36068
+ ...gantt.after === void 0 ? [] : [gantt.after]
36069
+ ];
36070
+ }
35276
36071
  function appDrawnFields(given, app) {
35277
36072
  const model = withAppRow(given, app);
35278
36073
  const drawn = /* @__PURE__ */ new Map();
@@ -35292,6 +36087,7 @@ function appDrawnFields(given, app) {
35292
36087
  ...[...app.register.where, ...app.register.opens].map((one) => one.field),
35293
36088
  ...app.register.lanes === void 0 ? [] : [app.register.lanes.field, app.register.lanes.start, ...app.register.lanes.end === void 0 ? [] : [app.register.lanes.end], ...(app.register.lanes.loads ?? []).map((load) => load.value)],
35294
36089
  ...app.register.roster?.of === void 0 ? [] : [app.register.roster.of],
36090
+ ...app.register.gantt === void 0 ? [] : ganttFields(app.register.gantt),
35295
36091
  ...app.register.readings.flatMap(readingFields),
35296
36092
  ...app.record.sections.flatMap((section) => [...section.fields, ...section.when.map((gate) => gate.field)]),
35297
36093
  ...resolvedBlocks(app.record).flatMap((block) => [
@@ -35335,8 +36131,18 @@ function appDrawnFields(given, app) {
35335
36131
  if (block.kind === "rows") {
35336
36132
  const child = entityOf(model, block.entity);
35337
36133
  const written = child === void 0 ? [] : child.fields.filter((field) => isAsked(field) && field.alias !== block.via).map((field) => field.alias);
35338
- add(block.entity, [...block.columns, ...block.create ?? [], ...block.where.map((one) => one.field), ...written]);
35339
- add(app.entity, [...blockTotals(model, app.entity, block.entity, block.via, block.columns, block.where).values(), ...block.of === void 0 ? [] : [block.of]]);
36134
+ add(block.entity, [
36135
+ ...block.columns,
36136
+ ...block.create ?? [],
36137
+ ...block.where.map((one) => one.field),
36138
+ ...block.filters.map(filterField),
36139
+ ...block.search ?? [],
36140
+ ...block.group === void 0 ? [] : [block.group],
36141
+ ...(block.sort ?? []).map((key) => key.field),
36142
+ ...written
36143
+ ]);
36144
+ const siblings = resolvedBlocks(app.record).flatMap((one) => one.kind === "rows" && one.entity === block.entity && one.via === block.via ? [one.where] : []);
36145
+ add(app.entity, [...blockTotals(model, app.entity, block.entity, block.via, block.columns, block.where, siblings).values(), ...block.of === void 0 ? [] : [block.of]]);
35340
36146
  const under = block.under;
35341
36147
  const document = under === void 0 ? void 0 : entityOf(model, under.entity);
35342
36148
  if (under !== void 0 && document !== void 0) {
@@ -35367,6 +36173,10 @@ function appDrawnFields(given, app) {
35367
36173
  const written = /* @__PURE__ */ new Set([...writes.create ?? [], ...writes.update]);
35368
36174
  const entity = entityOf(model, alias2);
35369
36175
  for (const [field, rule] of Object.entries(model.write_rules?.[alias2]?.fields ?? {})) {
36176
+ if (rule.suggest !== void 0 && written.has(field)) {
36177
+ const offered = suggestRef(rule.suggest);
36178
+ add(offered.entity, [offered.field]);
36179
+ }
35370
36180
  if (rule.default_from === void 0 || !written.has(field) || entity === void 0) continue;
35371
36181
  const [via = "", source = ""] = rule.default_from.split(".");
35372
36182
  const link = fieldOf(entity, via);
@@ -35391,6 +36201,17 @@ function followLinks(model, drawn) {
35391
36201
  const alias2 = order[index];
35392
36202
  add(alias2, recordDisplayFields(recordDisplay(model, alias2)));
35393
36203
  const entity = entityOf(model, alias2);
36204
+ for (const [name, conditions] of Object.entries(recordDisplay(model, alias2)?.applies ?? {})) {
36205
+ if (entity === void 0 || !(drawn.get(alias2) ?? []).includes(name)) continue;
36206
+ for (const condition of Object.keys(conditions)) {
36207
+ const shape = appliesCondition(model, entity, condition);
36208
+ if ("refused" in shape) continue;
36209
+ add(alias2, [condition, ...shape.through === void 0 ? [] : [shape.through.link]]);
36210
+ if (shape.through === void 0) continue;
36211
+ if (!drawn.has(shape.through.entity)) order.push(shape.through.entity);
36212
+ add(shape.through.entity, [shape.through.field]);
36213
+ }
36214
+ }
35394
36215
  if (entity !== void 0) {
35395
36216
  add(
35396
36217
  alias2,
@@ -35432,7 +36253,8 @@ function checkFilesShown(model) {
35432
36253
  ...display?.subtitle ?? [],
35433
36254
  ...app.register?.columns ?? [],
35434
36255
  ...sections.flatMap((section) => [...section.fields ?? [], ...(section.blocks ?? []).flatMap((block) => "files" in block ? block.files : [])]),
35435
- ...(app.acts ?? []).flatMap((act) => act.templates === void 0 || act.into === void 0 || !standing.has(act.alias) ? [] : [act.into])
36256
+ // The paper an act makes — one template's, or each of a set's — reads where the act stands.
36257
+ ...(app.acts ?? []).flatMap((act) => act.into === void 0 || !standing.has(act.alias) ? [] : [act.into])
35436
36258
  ];
35437
36259
  })
35438
36260
  );
@@ -35483,6 +36305,501 @@ function noteRegisterReadings(model, app, at2) {
35483
36305
  return [note("register.band-headline", `${at2}.register`, `states no \`readings\` over "${app.entity}", whose rows move through ${moving} \u2014 lead the band with the job's headline number`)];
35484
36306
  }
35485
36307
 
36308
+ // ../shared/src/formula.ts
36309
+ function getSyntheticValue(field) {
36310
+ switch (field.type) {
36311
+ case "number":
36312
+ return 1;
36313
+ case "autonumber":
36314
+ return "a";
36315
+ case "text":
36316
+ return "a";
36317
+ case "lookup": {
36318
+ switch ("lookup_field_type" in field ? field.lookup_field_type : void 0) {
36319
+ case "number":
36320
+ return 1;
36321
+ case "date":
36322
+ return "2024-01-15T00:00:00Z";
36323
+ case "boolean":
36324
+ return true;
36325
+ case "select":
36326
+ case "select_member":
36327
+ case "select_record_link":
36328
+ case "files":
36329
+ return ["a"];
36330
+ default:
36331
+ return "a";
36332
+ }
36333
+ }
36334
+ case "date":
36335
+ return "2024-01-15T00:00:00Z";
36336
+ case "boolean":
36337
+ return true;
36338
+ case "select":
36339
+ case "select_member":
36340
+ return "option";
36341
+ case "select_record_link":
36342
+ return [];
36343
+ case "files":
36344
+ return [];
36345
+ case "rollup":
36346
+ return ("aggregate_field_type" in field ? field.aggregate_field_type : void 0) === "date" ? "2024-01-15T00:00:00Z" : 1;
36347
+ case "formula": {
36348
+ switch ("output_type" in field.formula ? field.formula.output_type : void 0) {
36349
+ case "number":
36350
+ return 1;
36351
+ case "date":
36352
+ case "datetime":
36353
+ return "2024-01-15T00:00:00Z";
36354
+ case "boolean":
36355
+ return true;
36356
+ // Read as a select's cell is.
36357
+ case "select":
36358
+ return "option";
36359
+ case "text":
36360
+ case void 0:
36361
+ return "a";
36362
+ }
36363
+ return "a";
36364
+ }
36365
+ case "button":
36366
+ return null;
36367
+ default:
36368
+ return null;
36369
+ }
36370
+ }
36371
+ function isISODateString(value) {
36372
+ if (typeof value !== "string") return false;
36373
+ return isValid(parseISO(value));
36374
+ }
36375
+ function inferFormulaOutputType(expression, fields) {
36376
+ const syntheticData = {};
36377
+ for (const field of fields) {
36378
+ syntheticData[field.key] = getSyntheticValue(field);
36379
+ }
36380
+ const { evaluable } = prepareFormulaExpression(expression, fields);
36381
+ if (evaluable === null) return "text";
36382
+ const result = evaluateFormulaWithJS(evaluable, { data: syntheticData }, "UTC", false);
36383
+ if (result === null || result === void 0) {
36384
+ const sampled = sampleTypedResult(evaluable, syntheticData, expression, fields);
36385
+ if (sampled !== null) return sampled;
36386
+ return "text";
36387
+ }
36388
+ if (typeof result === "number") return "number";
36389
+ if (typeof result === "boolean") return "boolean";
36390
+ if (isISODateString(result)) {
36391
+ return formulaDateHasTimeOfDay(expression, fields, result) ? "datetime" : "date";
36392
+ }
36393
+ return "text";
36394
+ }
36395
+ function sampleTypedResult(evaluable, filled, expression, fields) {
36396
+ const referenced = referencedFieldKeys(expression, fields);
36397
+ if (referenced.length === 0) return null;
36398
+ const assignments = [];
36399
+ assignments.push({ ...filled, ...Object.fromEntries(referenced.map((k) => [k, null])) });
36400
+ for (const key of referenced) {
36401
+ assignments.push({ ...filled, [key]: null });
36402
+ }
36403
+ for (const data of assignments) {
36404
+ const result = evaluateFormulaWithJS(evaluable, { data }, "UTC", false);
36405
+ if (result === null || result === void 0) continue;
36406
+ if (typeof result === "string" && result.startsWith(FORMULA_ERROR_PREFIX)) continue;
36407
+ if (typeof result === "number") return "number";
36408
+ if (typeof result === "boolean") return "boolean";
36409
+ if (isISODateString(result)) {
36410
+ return formulaDateHasTimeOfDay(expression, fields, result) ? "datetime" : "date";
36411
+ }
36412
+ return "text";
36413
+ }
36414
+ return null;
36415
+ }
36416
+ function formulaDateHasTimeOfDay(expression, fields, result) {
36417
+ const referenced = new Set(referencedFieldKeys(expression, fields));
36418
+ for (const f of fields) {
36419
+ if (!referenced.has(f.key)) continue;
36420
+ if (f.type === "date" && (f.format === "datetime" || f.format === "datetime_range")) return true;
36421
+ if (f.type === "lookup" && f.lookup_field_type === "date" && (f.lookup_field_format === "datetime" || f.lookup_field_format === "datetime_range")) return true;
36422
+ if (f.type === "rollup" && (f.aggregate_field_format === "datetime" || f.aggregate_field_format === "datetime_range")) return true;
36423
+ if (f.type === "formula" && f.formula.output_type === "datetime") return true;
36424
+ }
36425
+ if (typeof result !== "string") return false;
36426
+ const m = /T(\d{2}):(\d{2})(?::(\d{2}))?/.exec(result);
36427
+ if (!m) return false;
36428
+ return m[1] !== "00" || m[2] !== "00" || (m[3] ?? "00") !== "00";
36429
+ }
36430
+ function readsFieldsOnlyThroughEmptinessPredicates(node) {
36431
+ if (node === void 0) return true;
36432
+ if (isFieldRead(node)) return false;
36433
+ switch (node.type) {
36434
+ case "Invoke": {
36435
+ const args = node.arguments ?? [];
36436
+ const predicate = node.method === void 0 && node.receiver.type === "ID" && EMPTINESS_PREDICATES.has(node.receiver.value);
36437
+ return (predicate || readsFieldsOnlyThroughEmptinessPredicates(node.receiver)) && args.every(
36438
+ (arg) => predicate && isFieldRead(arg) || readsFieldsOnlyThroughEmptinessPredicates(arg)
36439
+ );
36440
+ }
36441
+ case "Unary":
36442
+ return readsFieldsOnlyThroughEmptinessPredicates(node.child);
36443
+ case "Binary":
36444
+ return readsFieldsOnlyThroughEmptinessPredicates(node.left) && readsFieldsOnlyThroughEmptinessPredicates(node.right);
36445
+ case "Getter":
36446
+ return readsFieldsOnlyThroughEmptinessPredicates(node.receiver);
36447
+ case "Index":
36448
+ return readsFieldsOnlyThroughEmptinessPredicates(node.receiver) && readsFieldsOnlyThroughEmptinessPredicates(node.argument);
36449
+ case "Ternary":
36450
+ return readsFieldsOnlyThroughEmptinessPredicates(node.condition) && readsFieldsOnlyThroughEmptinessPredicates(node.trueExpr) && readsFieldsOnlyThroughEmptinessPredicates(node.falseExpr);
36451
+ case "List":
36452
+ return (node.items ?? []).every(readsFieldsOnlyThroughEmptinessPredicates);
36453
+ case "Map":
36454
+ return Object.values(node.entries ?? {}).every(readsFieldsOnlyThroughEmptinessPredicates);
36455
+ case "ArrowFunction":
36456
+ return readsFieldsOnlyThroughEmptinessPredicates(node.body);
36457
+ case "ID":
36458
+ return node.value !== "data";
36459
+ case "Literal":
36460
+ case "Empty":
36461
+ return true;
36462
+ }
36463
+ }
36464
+ function isFieldRead(node) {
36465
+ return node !== void 0 && node.type === "Index" && node.receiver.type === "ID" && node.receiver.value === "data";
36466
+ }
36467
+ function evaluateFormulaWithJS(preparedExpression, data, timezone, everyReferenceEmpty) {
36468
+ try {
36469
+ if (everyReferenceEmpty && !readsFieldsOnlyThroughEmptinessPredicates(parseExpression(preparedExpression))) {
36470
+ return null;
36471
+ }
36472
+ const result = evaluateJsExpression(preparedExpression, data, void 0, timezone);
36473
+ if (result === null || result === void 0) {
36474
+ return null;
36475
+ }
36476
+ if (typeof result === "number" || typeof result === "string" || typeof result === "boolean") {
36477
+ return result;
36478
+ }
36479
+ return `${FORMULA_ERROR_PREFIX}Unexpected result type`;
36480
+ } catch (error52) {
36481
+ return `${FORMULA_ERROR_PREFIX}Formula evaluation error: ${error52 instanceof Error ? error52.message : String(error52)}`;
36482
+ }
36483
+ }
36484
+
36485
+ // ../shared/src/schemas/contract_filter_binding.ts
36486
+ function remapContractFilterGroup(group, entity, resolve) {
36487
+ return { ...group, children: group.children.map((child) => remapContractFilter(child, entity, resolve)) };
36488
+ }
36489
+ function remapContractFilter(node, entity, resolve) {
36490
+ if (node.node_type === "group") return remapContractFilterGroup(node, entity, resolve);
36491
+ if (node.node_type === "traversal") {
36492
+ return {
36493
+ ...node,
36494
+ path: node.path.map((hop) => resolve.qualified(hop).key),
36495
+ condition: remapTraversalCondition(node.condition, resolve)
36496
+ };
36497
+ }
36498
+ const roleOf = resolve.role;
36499
+ if (node.type === "current_member" && roleOf !== void 0 && Array.isArray(node.value)) {
36500
+ return { ...node, value: node.value.map((one) => typeof one === "string" ? roleOf(one) : one) };
36501
+ }
36502
+ if (entity === null || node.field_key === void 0 || node.field_key === "") return node;
36503
+ return {
36504
+ ...node,
36505
+ field_key: resolve.field(entity, node.field_key),
36506
+ value: remapOptionValue(node.value, entity, node.field_key, resolve),
36507
+ ...remapUnitOption(node.unit_option, resolve)
36508
+ };
36509
+ }
36510
+ function remapTraversalCondition(condition, resolve) {
36511
+ if (condition.field_key === void 0 || condition.field_key === "") return condition;
36512
+ const { key, entity, field } = resolve.qualified(condition.field_key);
36513
+ return {
36514
+ ...condition,
36515
+ field_key: key,
36516
+ value: remapOptionValue(condition.value, entity, field, resolve),
36517
+ ...remapUnitOption(condition.unit_option, resolve)
36518
+ };
36519
+ }
36520
+ function remapUnitOption(unitOption, resolve) {
36521
+ if (unitOption === void 0) return {};
36522
+ const parsed = parseOptionKeyName(unitOption);
36523
+ return { unit_option: parsed === null ? unitOption : resolve.option(parsed.entity, parsed.field, parsed.option) };
36524
+ }
36525
+ function remapOptionValue(value, entity, field, resolve) {
36526
+ if (!resolve.isSelect(entity, field)) return value;
36527
+ const one = (candidate) => typeof candidate === "string" ? resolve.option(entity, field, candidate) : candidate;
36528
+ return Array.isArray(value) ? value.map(one) : one(value);
36529
+ }
36530
+
36531
+ // ../shared/src/app_compile/model_error.ts
36532
+ var ModelError = class extends Error {
36533
+ constructor(message2) {
36534
+ super(message2);
36535
+ this.name = "ModelError";
36536
+ }
36537
+ };
36538
+ function fail2(message2) {
36539
+ throw new ModelError(message2);
36540
+ }
36541
+
36542
+ // ../shared/src/app_compile/synthetic_workspace.ts
36543
+ function tableId(t) {
36544
+ return `tbl_${t}`;
36545
+ }
36546
+ function syntheticFields(entities) {
36547
+ const binding = bindAliases(entities);
36548
+ return entities.map((entity) => entity.fields.map((field) => syntheticField(entity, field, binding)));
36549
+ }
36550
+ function syntheticFormula(entities, entity, expression) {
36551
+ const binding = bindAliases(entities);
36552
+ return bindContractFormula(expression, {
36553
+ field: (alias2) => binding.field.get(fieldKeyName(entity, alias2)) ?? alias2,
36554
+ option: (field, option) => binding.option.get(optionKeyName(entity, field, option)) ?? option
36555
+ });
36556
+ }
36557
+ function bindAliases(entities) {
36558
+ const table = /* @__PURE__ */ new Map();
36559
+ const field = /* @__PURE__ */ new Map();
36560
+ const option = /* @__PURE__ */ new Map();
36561
+ const declared = /* @__PURE__ */ new Map();
36562
+ entities.forEach((entity, t) => {
36563
+ table.set(entity.alias, tableId(t));
36564
+ entity.fields.forEach((one, f) => {
36565
+ field.set(fieldKeyName(entity.alias, one.alias), `fld_${t}_${f}`);
36566
+ declared.set(fieldKeyName(entity.alias, one.alias), one);
36567
+ (contractSelectOptions(one) ?? []).forEach((choice, o) => {
36568
+ option.set(optionKeyName(entity.alias, one.alias, choice.alias), `opt_${t}_${f}_${o}`);
36569
+ });
36570
+ });
36571
+ });
36572
+ return { table, field, option, declared, entities };
36573
+ }
36574
+ function requireField(binding, entity, alias2, at2) {
36575
+ const key = binding.field.get(fieldKeyName(entity, alias2));
36576
+ if (key === void 0) fail2(`${at2} names "${alias2}", which entity "${entity}" does not declare`);
36577
+ return key;
36578
+ }
36579
+ function perRowKeys(binding, entity, field, at2) {
36580
+ const select = contractFigureSelect(field, entity, binding.entities);
36581
+ if (select === void 0) return {};
36582
+ const key = requireField(binding, entity.alias, select.alias, `${at2}.${select.vocabulary}_field`);
36583
+ return select.vocabulary === "unit" ? { unit_field: key } : { currency_field: key };
36584
+ }
36585
+ function requireTarget(binding, entity, alias2, at2) {
36586
+ const found = linkTarget(entity, binding.entities, alias2);
36587
+ if (found === void 0) {
36588
+ fail2(`${at2} reaches through "${alias2}", which is not a link on entity "${entity.alias}"`);
36589
+ }
36590
+ return found;
36591
+ }
36592
+ function syntheticField(entity, field, binding) {
36593
+ const at2 = `${entity.alias}.${field.alias}`;
36594
+ const base = {
36595
+ key: requireField(binding, entity.alias, field.alias, at2),
36596
+ name: field.label,
36597
+ description: field.description ?? "",
36598
+ ...field.required !== void 0 && { required: field.required }
36599
+ };
36600
+ switch (field.type) {
36601
+ case "text":
36602
+ return { ...base, type: "text", ...field.format !== void 0 && { format: field.format } };
36603
+ case "number":
36604
+ return {
36605
+ ...base,
36606
+ type: "number",
36607
+ format: field.format ?? "number",
36608
+ ...field.currency !== void 0 && { currency: field.currency },
36609
+ ...field.unit !== void 0 && { unit: field.unit },
36610
+ ...perRowKeys(binding, entity, field, at2)
36611
+ };
36612
+ case "date":
36613
+ return {
36614
+ ...base,
36615
+ type: "date",
36616
+ ...field.format !== void 0 && { format: field.format },
36617
+ ...field.timezone !== void 0 && { timezone: field.timezone }
36618
+ };
36619
+ case "boolean":
36620
+ return { ...base, type: "boolean" };
36621
+ case "files":
36622
+ return { ...base, type: "files" };
36623
+ case "select_member":
36624
+ return { ...base, type: "select_member", ...field.multi !== void 0 && { multi: field.multi } };
36625
+ case "select":
36626
+ return {
36627
+ ...base,
36628
+ type: "select",
36629
+ options: mintedOptions(binding, entity.alias, field.alias, field.options, at2),
36630
+ ...field.multi !== void 0 && { multi: field.multi }
36631
+ };
36632
+ case "autonumber":
36633
+ return {
36634
+ ...base,
36635
+ type: "autonumber",
36636
+ ...field.prefix !== void 0 && { prefix: field.prefix },
36637
+ ...field.padding !== void 0 && { padding: field.padding },
36638
+ ...field.template !== void 0 && { template: field.template }
36639
+ };
36640
+ case "select_record_link": {
36641
+ const table = binding.table.get(field.target_entity);
36642
+ if (table === void 0) fail2(`${at2} points at entity "${field.target_entity}", which this model does not declare`);
36643
+ const displays = field.display_field_aliases;
36644
+ return {
36645
+ ...base,
36646
+ type: "select_record_link",
36647
+ table_id: table,
36648
+ // A DISPLAY RULE ONLY WHERE THE MODEL STATES ONE, the way the copy path
36649
+ // writes it: unstated, the platform settles the list when the field is
36650
+ // created, and nothing this synthesis feeds reads it.
36651
+ display_field_keys: displays === void 0 ? [] : displays.map((alias2) => requireField(binding, field.target_entity, alias2, `${at2}.display_field_aliases`)),
36652
+ ...field.cardinality !== void 0 && { cardinality: field.cardinality }
36653
+ };
36654
+ }
36655
+ case "formula": {
36656
+ const { options, ...formula } = field.formula;
36657
+ return {
36658
+ ...base,
36659
+ type: "formula",
36660
+ // Alias → field KEY, and `{field:option}` → that option's key: the
36661
+ // expression the engine runs addresses opaque keys, exactly as the copy
36662
+ // writes it.
36663
+ formula: {
36664
+ ...formula,
36665
+ expression: bindContractFormula(field.formula.expression, {
36666
+ field: (alias2) => requireField(binding, entity.alias, alias2, `${at2}.formula`),
36667
+ option: (select, option) => requireOption(binding, entity.alias, select, option, `${at2}.formula`)
36668
+ }),
36669
+ ...perRowKeys(binding, entity, field, `${at2}.formula`),
36670
+ ...options === void 0 ? {} : { output_type: "select", options: mintedOptions(binding, entity.alias, field.alias, options, at2) }
36671
+ }
36672
+ };
36673
+ }
36674
+ case "rollup": {
36675
+ const target = requireTarget(binding, entity, field.source_field_alias, at2);
36676
+ const aggregate = field.aggregate_option;
36677
+ const aggregated = aggregate.field_key === void 0 ? void 0 : requireDeclared(binding, target.alias, aggregate.field_key, `${at2}.aggregate_option`);
36678
+ return {
36679
+ ...base,
36680
+ type: "rollup",
36681
+ source_field_key: requireField(binding, entity.alias, field.source_field_alias, at2),
36682
+ aggregate_option: aggregate.field_key === void 0 ? aggregate : { ...aggregate, field_key: requireField(binding, target.alias, aggregate.field_key, `${at2}.aggregate_option`) },
36683
+ // WHAT THE CELL HOLDS, derived the way the write path derives it: the
36684
+ // type from the OPERATION alone, the format, currency and unit from the
36685
+ // aggregated field only where the operation keeps its unit. Absent, a
36686
+ // `latest` over a datetime reads as a day and a `sum` of a VND column
36687
+ // draws as a bare count.
36688
+ aggregate_field_type: rollupOutputType(aggregate.operation),
36689
+ ...rollupOutputFormat(
36690
+ aggregate.operation,
36691
+ aggregated === void 0 ? {} : resolvedFormatOf(aggregated, target, binding.entities),
36692
+ perRowKeys(binding, entity, field, at2)
36693
+ ),
36694
+ // The filter reads the LINKED entity's fields and options, so it
36695
+ // resolves against that entity rather than this one.
36696
+ ...field.filter !== void 0 ? { filter: remapFilter(field.filter, target.alias, binding, `${at2}.filter`) } : {}
36697
+ };
36698
+ }
36699
+ case "lookup": {
36700
+ const target = requireTarget(binding, entity, field.source_field_alias, at2);
36701
+ const surfaced = requireDeclared(binding, target.alias, field.lookup_field_alias, at2);
36702
+ const resolved = resolvedFieldType(surfaced, target, binding.entities);
36703
+ const carried = resolvedFormatOf(surfaced, target, binding.entities);
36704
+ return {
36705
+ ...base,
36706
+ type: "lookup",
36707
+ source_field_key: requireField(binding, entity.alias, field.source_field_alias, at2),
36708
+ lookup_field_key: requireField(binding, target.alias, field.lookup_field_alias, at2),
36709
+ ...resolved !== void 0 && { lookup_field_type: resolved },
36710
+ ...carried.format !== void 0 && { lookup_field_format: carried.format },
36711
+ ...carried.currency !== void 0 && { lookup_field_currency: carried.currency },
36712
+ ...carried.unit !== void 0 && { lookup_field_unit: carried.unit },
36713
+ ...resolved === "select" && {
36714
+ lookup_field_options: surfacedOptions(binding, carried, at2)
36715
+ },
36716
+ ...field.order_by !== void 0 && {
36717
+ order_by: {
36718
+ field_key: requireField(binding, target.alias, field.order_by.field_key, `${at2}.order_by`),
36719
+ direction: field.order_by.direction
36720
+ }
36721
+ }
36722
+ };
36723
+ }
36724
+ }
36725
+ }
36726
+ function mintedOptions(binding, entity, field, options, at2) {
36727
+ return options.map((option) => ({
36728
+ key: requireOption(binding, entity, field, option.alias, at2),
36729
+ name: option.label,
36730
+ color: option.color,
36731
+ ...option.mark === void 0 ? {} : { mark: option.mark }
36732
+ }));
36733
+ }
36734
+ function surfacedOptions(binding, carried, at2) {
36735
+ const owner = carried.options;
36736
+ if (owner === void 0) {
36737
+ fail2(`${at2} surfaces a select, but this model states no options for the column it reaches`);
36738
+ }
36739
+ return mintedOptions(binding, owner.entity, owner.field, owner.options, at2);
36740
+ }
36741
+ var SURFACE_HOPS = 8;
36742
+ function resolvedFormatOf(field, entity, entities, hops = 0) {
36743
+ if (hops >= SURFACE_HOPS) return {};
36744
+ switch (field.type) {
36745
+ case "number":
36746
+ return { format: field.format ?? "number", currency: field.currency, unit: field.unit };
36747
+ case "text":
36748
+ case "date":
36749
+ return { format: field.format };
36750
+ case "select":
36751
+ return { options: { entity: entity.alias, field: field.alias, options: field.options } };
36752
+ case "formula":
36753
+ if (field.formula.options !== void 0) return { options: { entity: entity.alias, field: field.alias, options: field.formula.options } };
36754
+ return {
36755
+ format: field.formula.output_type === "datetime" ? "datetime" : field.formula.format,
36756
+ currency: field.formula.currency,
36757
+ unit: field.formula.unit
36758
+ };
36759
+ case "rollup": {
36760
+ const { operation, field_key } = field.aggregate_option;
36761
+ const target = linkTarget(entity, entities, field.source_field_alias);
36762
+ const aggregated = field_key === void 0 ? void 0 : target?.fields.find((one) => one.alias === field_key);
36763
+ const inherited = target === void 0 || aggregated === void 0 ? {} : resolvedFormatOf(aggregated, target, entities, hops + 1);
36764
+ const { aggregate_field_format, aggregate_field_currency, aggregate_field_unit } = rollupOutputFormat(operation, inherited);
36765
+ return { format: aggregate_field_format, currency: aggregate_field_currency, unit: aggregate_field_unit };
36766
+ }
36767
+ case "lookup": {
36768
+ const target = linkTarget(entity, entities, field.source_field_alias);
36769
+ const shown = target?.fields.find((one) => one.alias === field.lookup_field_alias);
36770
+ return target === void 0 || shown === void 0 ? {} : resolvedFormatOf(shown, target, entities, hops + 1);
36771
+ }
36772
+ default:
36773
+ return {};
36774
+ }
36775
+ }
36776
+ function requireDeclared(binding, entity, alias2, at2) {
36777
+ const found = binding.declared.get(fieldKeyName(entity, alias2));
36778
+ if (found === void 0) fail2(`${at2} names "${alias2}", which entity "${entity}" does not declare`);
36779
+ return found;
36780
+ }
36781
+ function requireOption(binding, entity, field, option, at2) {
36782
+ const key = binding.option.get(optionKeyName(entity, field, option));
36783
+ if (key === void 0) fail2(`${at2} names option "${option}", which "${entity}.${field}" does not declare`);
36784
+ return key;
36785
+ }
36786
+ function remapFilter(group, entity, binding, at2) {
36787
+ const resolver = {
36788
+ field: (owner, alias2) => requireField(binding, owner, alias2, at2),
36789
+ qualified: (position) => {
36790
+ const parsed = parseFieldKeyName(position);
36791
+ if (parsed === null) fail2(`${at2} addresses "${position}", which is not an "entity.field" alias`);
36792
+ return { key: requireField(binding, parsed.entity, parsed.field, at2), ...parsed };
36793
+ },
36794
+ option: (owner, field, alias2) => requireOption(binding, owner, field, alias2, `${owner}.${field}`),
36795
+ isSelect: (owner, field) => {
36796
+ const declared = binding.declared.get(fieldKeyName(owner, field));
36797
+ return declared !== void 0 && contractSelectOptions(declared) !== void 0;
36798
+ }
36799
+ };
36800
+ return remapContractFilterGroup(group, entity, resolver);
36801
+ }
36802
+
35486
36803
  // ../shared/src/app_workflow_inputs.ts
35487
36804
  var RECORD_INPUT = "record_id";
35488
36805
  var RECORDS_INPUT = "record_ids";
@@ -35575,11 +36892,12 @@ function checkAct(model, entity, act, at2) {
35575
36892
  if (field === void 0) continue;
35576
36893
  const written = actSetValue(field, value, inputs);
35577
36894
  if ("refused" in written) findings.push(error51("act.set", `${at2}.set.${name}`, written.refused));
36895
+ else if (written.kind === "formula") findings.push(...checkSetFormula(model, entity, field, written.expression, `${at2}.set.${name}`));
35578
36896
  else if (written.kind === "clear" && stages.has(name)) {
35579
36897
  findings.push(error51("act.set-milestone", `${at2}.set.${name}`, `clears the milestone "${name}" \u2014 a milestone is cleared by unticking it, which clears every later one`));
35580
36898
  }
35581
36899
  }
35582
- if (act.template !== void 0) findings.push(...checkTemplate(model, act.template, `${at2}.template`, "act"));
36900
+ if (act.template !== void 0) findings.push(...checkTemplate(model, act.template, `${at2}.template`));
35583
36901
  if (act.templates !== void 0) findings.push(...checkTemplates(model, entity, act, at2));
35584
36902
  if (act.into !== void 0) {
35585
36903
  const into = own(act.into, `${at2}.into`);
@@ -35602,7 +36920,7 @@ function checkTemplates(model, entity, act, at2) {
35602
36920
  }
35603
36921
  for (const [index, one] of templates.entries()) {
35604
36922
  const each = `${path15}.${index}`;
35605
- findings.push(...checkTemplate(model, one.template, `${each}.template`, "act"), ...checkWhen(model.entities, entity, one.when, `${each}.when`, "offer"));
36923
+ findings.push(...checkTemplate(model, one.template, `${each}.template`), ...checkWhen(model.entities, entity, one.when, `${each}.when`, "offer"));
35606
36924
  if (templates.findIndex((other) => other.template === one.template) !== index) findings.push(error51("act.templates", `${each}.template`, `names "${one.template}" twice \u2014 each template is one paper of the set`));
35607
36925
  }
35608
36926
  return findings;
@@ -35885,6 +37203,28 @@ function checkFootNeeds(model, entity, section, drawn, act, path15) {
35885
37203
  return [error51("act.requires-written", at2, `"${act.alias}" needs "${name}", which no act offered together with it writes \u2014 it reads locked while "${act.alias}" waits; have "${act.alias}" ask it`)];
35886
37204
  });
35887
37205
  }
37206
+ var FORMULA_INTO = {
37207
+ number: ["number"],
37208
+ date: ["date", "datetime"],
37209
+ text: ["text"],
37210
+ boolean: ["boolean"]
37211
+ };
37212
+ function checkSetFormula(model, entity, field, expression, at2) {
37213
+ const display = recordDisplay(model, entity.alias);
37214
+ if (field.alias === display?.status?.field || (milestonesOf(display) ?? []).some((stage) => stage.field === field.alias)) {
37215
+ return [error51("act.set-formula", at2, `computes "${field.alias}", which moves the work \u2014 an act moves it to an option or stamps it, never by a formula`)];
37216
+ }
37217
+ const refused = formulaExpressionRefusals(expression);
37218
+ if (refused.length > 0) return refused.map((message2) => error51("act.set-formula", at2, message2));
37219
+ const unknown2 = contractFormulaFieldRefs({ alias: field.alias, formula: { expression } }).filter((name) => fieldOf(entity, name) === void 0);
37220
+ if (unknown2.length > 0) return [error51("act.set-formula", at2, `reads ${unknown2.map((name) => `"${name}"`).join(", ")}, which ${unknown2.length === 1 ? "is not a field" : "are not fields"} of "${entity.alias}"`)];
37221
+ const into = FORMULA_INTO[field.type];
37222
+ if (into === void 0) return [error51("act.set-formula", at2, `computes the ${field.type} "${field.alias}" \u2014 a formula computes a number, a date, text or a yes/no`)];
37223
+ const index = model.entities.findIndex((one) => one.alias === entity.alias);
37224
+ const fields = syntheticFields(model.entities)[index] ?? [];
37225
+ const computed = inferFormulaOutputType(syntheticFormula(model.entities, entity.alias, expression), fields);
37226
+ return into.includes(computed) ? [] : [error51("act.set-formula", at2, `computes a ${computed} into the ${field.type} "${field.alias}" \u2014 the formula computes what the field holds`)];
37227
+ }
35888
37228
 
35889
37229
  // ../shared/src/schemas/app_model_resolve_tasks.ts
35890
37230
  function tasksOf(model, app, resolved) {
@@ -36414,6 +37754,7 @@ function translateAct(act, lining, listed) {
36414
37754
  for (const [alias2, value] of Object.entries(act.set ?? {})) {
36415
37755
  const own = to(alias2);
36416
37756
  if (own === void 0) return { dropped: `it sets "${alias2}", which "${book.alias}" lacks` };
37757
+ if (typeof value === "object" && value !== null) return { dropped: `it computes "${alias2}" by a formula over the app's own fields` };
36417
37758
  const option = typeof value === "string" && value !== SET_NOW && value !== SET_ME && !value.startsWith(SET_INPUT) && optionsOf(fieldOf(book, own)) !== void 0;
36418
37759
  if (option && held(alias2, [value]).length === 0) return { dropped: `it sets "${alias2}" to "${value}", an option "${book.alias}.${own}" lacks` };
36419
37760
  set2[own] = value;
@@ -36469,7 +37810,7 @@ var whereOf = (stated) => Object.entries(stated ?? {}).map(([field, options]) =>
36469
37810
  function resolveModelApp(given, app) {
36470
37811
  const model = withAppRow(given, app);
36471
37812
  const entity = entityOf(model, app.entity);
36472
- if (entity === void 0) throw new Error(`resolveModelApp: "${app.entity}" is not an entity of this model \u2014 validate it first`);
37813
+ if (entity === void 0) fail2(`resolveModelApp: "${app.entity}" is not an entity of this model \u2014 validate it first`);
36473
37814
  const display = model.records?.[entity.alias];
36474
37815
  const setBy = actSetFields(model, entity.alias);
36475
37816
  const writes = app.writes === "children" ? "children" : "record";
@@ -36489,7 +37830,8 @@ function resolveModelApp(given, app) {
36489
37830
  ...splitEntry(model, app, section.acts ?? [], at2),
36490
37831
  actsLast: false,
36491
37832
  when: whenConditions(section.when),
36492
- ...section.page === true ? { page: pageSegment(section.title) } : {}
37833
+ ...section.page === true ? { page: pageSegment(section.title) } : {},
37834
+ ...section.side === true ? { side: true } : {}
36493
37835
  };
36494
37836
  });
36495
37837
  if (statedSections === void 0) {
@@ -36504,15 +37846,16 @@ function resolveModelApp(given, app) {
36504
37846
  const acts2 = (app.acts ?? []).flatMap((act) => act.of === void 0 && (act.on ?? "record") === "record" ? [act.alias] : []);
36505
37847
  sections = fields.length === 0 && acts2.length === 0 ? [] : [{ at: [], fields, blocks: [], acts: acts2, entry: [], actsLast: false, when: [] }];
36506
37848
  }
36507
- const door = app.record?.door ?? (sections.some((section) => section.at.length > 0 || section.page !== void 0) || sections.length > 3 ? "page" : "drawer");
37849
+ const door = app.record?.door ?? (sections.some((section) => section.at.length > 0 || section.page !== void 0 || section.side === true) || sections.length > 3 ? "page" : "drawer");
36508
37850
  if (app.record?.door === void 0) defaulted.push("record.door");
36509
- const layout = app.register?.layout ?? "table";
37851
+ const registerLayout = app.register?.layout ?? "table";
36510
37852
  if (app.register?.layout === void 0) defaulted.push("register.layout");
36511
37853
  const roster = app.register?.roster === void 0 ? void 0 : resolveRoster(model, entity.alias, app.register.roster, app.register.expect, app.register.of);
36512
37854
  const lanes = app.register?.lanes === void 0 ? void 0 : resolveLanes(model, entity.alias, app.register.lanes, app.register.loads, scope);
37855
+ const gantt = app.register?.gantt === void 0 ? void 0 : ganttShape(model, entity.alias, app.register.gantt);
37856
+ if (gantt !== void 0 && "refused" in gantt) fail2(`resolveModelApp: the gantt ${gantt.refused.map((one) => one.reason).join("; ")} \u2014 validate it first`);
36513
37857
  const where = whenConditions(app.register?.where);
36514
- const statedSort = registerSortKeys(app.register?.sort);
36515
- let sort = statedSort.length === 0 ? void 0 : statedSort.map((key) => ({ field: key.field, ...key.desc === true ? { desc: true } : {} }));
37858
+ let sort = statedSort(app.register?.sort);
36516
37859
  if (sort === void 0) {
36517
37860
  defaulted.push("register.sort");
36518
37861
  const due = display?.due?.[0];
@@ -36559,17 +37902,19 @@ function resolveModelApp(given, app) {
36559
37902
  ...sort === void 0 ? {} : { sort },
36560
37903
  ...app.register?.group === void 0 ? {} : { group: app.register.group },
36561
37904
  tabs: (app.register?.tabs ?? []).map((tab) => {
36562
- const keys2 = registerSortKeys(tab.sort).map((key) => ({ field: key.field, ...key.desc === true ? { desc: true } : {} }));
37905
+ const keys2 = statedSort(tab.sort);
36563
37906
  const term = tab.in !== void 0 ? { kind: "in", field: tab.in } : tab.open === void 0 ? void 0 : { kind: "open", ...tab.open };
36564
- return { tab: tab.tab, label: tab.label, ...term === void 0 ? {} : { term }, where: narrowingConditions(tab.where), ...keys2.length === 0 ? {} : { sort: keys2 } };
37907
+ return { tab: tab.tab, label: tab.label, ...term === void 0 ? {} : { term }, where: narrowingConditions(tab.where), ...keys2 === void 0 ? {} : { sort: keys2 } };
36565
37908
  }),
36566
37909
  ...app.register?.period === void 0 ? {} : { period: app.register.period },
36567
- layout,
37910
+ layout: registerLayout,
36568
37911
  ...roster === void 0 ? {} : { roster },
36569
37912
  ...lanes === void 0 ? {} : { lanes },
37913
+ ...gantt === void 0 ? {} : { gantt },
36570
37914
  where,
36571
37915
  opens: narrowingConditions(app.register?.opens),
36572
37916
  ...create === void 0 || create.length === 0 ? {} : { create },
37917
+ ...app.register?.remove === void 0 ? {} : { remove: app.register.remove },
36573
37918
  ...stated === void 0 ? {} : {
36574
37919
  export: stated === true ? true : stated.map((report) => ({
36575
37920
  label: report.label,
@@ -36637,7 +37982,7 @@ function resolveDashboard(model, app) {
36637
37982
  }
36638
37983
  function resolveRoster(model, parent, alias2, expect, of) {
36639
37984
  const shape = rosterShape(model, parent, alias2, expect === void 0);
36640
- if ("refused" in shape) throw new Error(`resolveModelApp: the roster ${shape.refused} \u2014 validate it first`);
37985
+ if ("refused" in shape) fail2(`resolveModelApp: the roster ${shape.refused} \u2014 validate it first`);
36641
37986
  const child = entityOf(model, shape.entity);
36642
37987
  const key = expect ?? shape.day;
36643
37988
  const keyed = child === void 0 || key === void 0 ? void 0 : fieldOf(child, key);
@@ -36646,7 +37991,7 @@ function resolveRoster(model, parent, alias2, expect, of) {
36646
37991
  }
36647
37992
  function resolveLanes(model, parent, alias2, stated, scope) {
36648
37993
  const shape = lanesShape(model, parent, alias2);
36649
- if ("refused" in shape) throw new Error(`resolveModelApp: the lanes ${shape.refused} \u2014 validate it first`);
37994
+ if ("refused" in shape) fail2(`resolveModelApp: the lanes ${shape.refused} \u2014 validate it first`);
36650
37995
  const lane = entityOf(model, shape.entity);
36651
37996
  const folders = scope === void 0 || lane === void 0 ? [] : lane.fields.filter((field) => field.type === "select_record_link" && field.cardinality === "one" && field.target_entity === scope.entity);
36652
37997
  const [folder] = folders.length === 1 ? folders : [];
@@ -36654,7 +37999,7 @@ function resolveLanes(model, parent, alias2, stated, scope) {
36654
37999
  if (stated === void 0) return { ...shape, ...filed };
36655
38000
  const { loads, refused } = laneLoads(model, parent, shape, stated);
36656
38001
  const [first2] = refused;
36657
- if (first2 !== void 0) throw new Error(`resolveModelApp: the load "${first2.value}" ${first2.reason} \u2014 validate it first`);
38002
+ if (first2 !== void 0) fail2(`resolveModelApp: the load "${first2.value}" ${first2.reason} \u2014 validate it first`);
36658
38003
  return { ...shape, loads, ...filed };
36659
38004
  }
36660
38005
  function keptFields(entity, where) {
@@ -36666,9 +38011,13 @@ function keptFields(entity, where) {
36666
38011
  function scopeOf(entity, field) {
36667
38012
  if (field === void 0) return void 0;
36668
38013
  const link = fieldOf(entity, field);
36669
- if (link?.type !== "select_record_link") throw new Error(`resolveModelApp: scope "${field}" is not a link of "${entity.alias}" \u2014 validate it first`);
38014
+ if (link?.type !== "select_record_link") fail2(`resolveModelApp: scope "${field}" is not a link of "${entity.alias}" \u2014 validate it first`);
36670
38015
  return { field, entity: link.target_entity };
36671
38016
  }
38017
+ function statedSort(sort) {
38018
+ const keys2 = registerSortKeys(sort);
38019
+ return keys2.length === 0 ? void 0 : keys2.map((key) => ({ field: key.field, ...key.desc === true ? { desc: true } : {} }));
38020
+ }
36672
38021
  function resolveBlock(model, parent, block, at2) {
36673
38022
  const defaulted = [];
36674
38023
  const when = whenConditions(block.when);
@@ -36682,18 +38031,18 @@ function resolveBlock(model, parent, block, at2) {
36682
38031
  const reading = figure.reading;
36683
38032
  defaulted.push(...figure.defaulted);
36684
38033
  const child = entityOf(model, reading.entity);
36685
- if (child === void 0) throw new Error(`resolveModelApp: block ${at2} reads "${reading.entity}", which is not an entity \u2014 validate it first`);
38034
+ if (child === void 0) fail2(`resolveModelApp: block ${at2} reads "${reading.entity}", which is not an entity \u2014 validate it first`);
36686
38035
  const link = childLink(child, parent.alias, block.via);
36687
- if ("refused" in link) throw new Error(`resolveModelApp: block ${at2} ${link.refused}`);
38036
+ if ("refused" in link) fail2(`resolveModelApp: block ${at2} ${link.refused}`);
36688
38037
  return { block: { kind: "reading", reading, via: link.link, when }, defaulted };
36689
38038
  }
36690
38039
  if (isAgendaBlock(block)) {
36691
38040
  const child = entityOf(model, block.agenda);
36692
- if (child === void 0) throw new Error(`resolveModelApp: block ${at2} reads "${block.agenda}", which is not an entity \u2014 validate it first`);
38041
+ if (child === void 0) fail2(`resolveModelApp: block ${at2} reads "${block.agenda}", which is not an entity \u2014 validate it first`);
36693
38042
  const link = childLink(child, parent.alias, block.via);
36694
- if ("refused" in link) throw new Error(`resolveModelApp: block ${at2} ${link.refused}`);
38043
+ if ("refused" in link) fail2(`resolveModelApp: block ${at2} ${link.refused}`);
36695
38044
  const axis = agendaAxis(model, child);
36696
- if ("refused" in axis) throw new Error(`resolveModelApp: block ${at2} ${axis.refused}`);
38045
+ if ("refused" in axis) fail2(`resolveModelApp: block ${at2} ${axis.refused}`);
36697
38046
  const stated = block.create;
36698
38047
  const create = Array.isArray(stated) ? stated : stated === false || !peopleWrite(child) ? [] : defaultBlockCreate(model, child, [link.link], block.columns ?? []);
36699
38048
  if (stated === void 0 && create.length > 0) defaulted.push(`${at2}.create`);
@@ -36715,9 +38064,9 @@ function resolveBlock(model, parent, block, at2) {
36715
38064
  if ("rows" in block || "timeline" in block) {
36716
38065
  const childAlias = "rows" in block ? block.rows : block.timeline;
36717
38066
  const child = entityOf(model, childAlias);
36718
- if (child === void 0) throw new Error(`resolveModelApp: block ${at2} reads "${childAlias}", which is not an entity \u2014 validate it first`);
38067
+ if (child === void 0) fail2(`resolveModelApp: block ${at2} reads "${childAlias}", which is not an entity \u2014 validate it first`);
36719
38068
  const link = childLink(child, parent.alias, block.via);
36720
- if ("refused" in link) throw new Error(`resolveModelApp: block ${at2} ${link.refused}`);
38069
+ if ("refused" in link) fail2(`resolveModelApp: block ${at2} ${link.refused}`);
36721
38070
  const title = titled(block.title, child.label);
36722
38071
  const stated = block.create;
36723
38072
  if ("rows" in block) {
@@ -36738,6 +38087,12 @@ function resolveBlock(model, parent, block, at2) {
36738
38087
  where,
36739
38088
  ...block.expect === void 0 ? {} : { expect: block.expect },
36740
38089
  ...block.of === void 0 ? {} : { of: block.of },
38090
+ filters: block.filters ?? [],
38091
+ ...block.search === void 0 ? {} : { search: block.search },
38092
+ ...block.group === void 0 ? {} : { group: block.group },
38093
+ ...block.sort === void 0 ? {} : { sort: statedSort(block.sort) },
38094
+ ...block.edits === void 0 ? {} : { edits: block.edits },
38095
+ ...block.remove === void 0 ? {} : { remove: block.remove },
36741
38096
  ...under === void 0 ? {} : { under },
36742
38097
  when
36743
38098
  },
@@ -36745,7 +38100,7 @@ function resolveBlock(model, parent, block, at2) {
36745
38100
  };
36746
38101
  }
36747
38102
  const date5 = child.fields.find((field) => resolvedFieldType(field, child, model.entities) === "date")?.alias;
36748
- if (date5 === void 0) throw new Error(`resolveModelApp: block ${at2} reads "${child.alias}", which has no date \u2014 validate it first`);
38103
+ if (date5 === void 0) fail2(`resolveModelApp: block ${at2} reads "${child.alias}", which has no date \u2014 validate it first`);
36749
38104
  const by = child.fields.find((field) => field.type === "select_member")?.alias;
36750
38105
  const create = Array.isArray(stated) ? stated : stated === false || !peopleWrite(child) ? [] : defaultCreate(model, child, [link.link, date5, ...by === void 0 ? [] : [by]]);
36751
38106
  if (stated === void 0 && create.length > 0) defaulted.push(`${at2}.create`);
@@ -36802,7 +38157,7 @@ function resolveIntake(model, app, intake, fills, sections) {
36802
38157
  const papers = blocks.find((block) => block.entity === intake && block.expect === kind);
36803
38158
  const files = child?.fields.find((field) => field.type === "files");
36804
38159
  if (record2 === void 0 || child === void 0 || kind === void 0 || papers === void 0 || files === void 0) {
36805
- throw new Error(`resolveModelApp: "${intake}" is no child the record files papers as \u2014 validate it first`);
38160
+ fail2(`resolveModelApp: "${intake}" is no child the record files papers as \u2014 validate it first`);
36806
38161
  }
36807
38162
  const listed = new Set(blocks.map((block) => block.entity));
36808
38163
  const parties = /* @__PURE__ */ new Map();
@@ -36820,7 +38175,7 @@ function resolveIntake(model, app, intake, fills, sections) {
36820
38175
  }
36821
38176
  const block = blocks.find((one) => one.entity === name && one.expect === void 0 && one.under === void 0 && one.where.length === 0 && one.create !== void 0);
36822
38177
  const added = entityOf(model, name);
36823
- if (block === void 0 || added === void 0) throw new Error(`resolveModelApp: "${name}" rows are added in no plain rows block \u2014 validate it first`);
38178
+ if (block === void 0 || added === void 0) fail2(`resolveModelApp: "${name}" rows are added in no plain rows block \u2014 validate it first`);
36824
38179
  const filled = (block.create ?? []).filter((alias2) => {
36825
38180
  const field = fieldOf(added, alias2);
36826
38181
  return alias2 !== block.via && field !== void 0 && unfillable(model, added, field) === void 0;
@@ -36833,7 +38188,7 @@ function resolveIntake(model, app, intake, fills, sections) {
36833
38188
  fields,
36834
38189
  parties: [...parties].map(([link, named]) => {
36835
38190
  const field = fieldOf(record2, link);
36836
- if (field?.type !== "select_record_link") throw new Error(`resolveModelApp: "${link}" is not a link of "${record2.alias}" \u2014 validate it first`);
38191
+ if (field?.type !== "select_record_link") fail2(`resolveModelApp: "${link}" is not a link of "${record2.alias}" \u2014 validate it first`);
36837
38192
  return { link, entity: field.target_entity, fields: named };
36838
38193
  }),
36839
38194
  rows
@@ -36844,9 +38199,9 @@ function resolveRecord(model, app, recorded, fills, sections) {
36844
38199
  (one) => (one.kind === "timeline" || one.kind === "rows" && one.expect === void 0 && one.under === void 0 && one.where.length === 0) && one.entity === recorded.into
36845
38200
  );
36846
38201
  if (block === void 0 || block.kind !== "rows" && block.kind !== "timeline") {
36847
- throw new Error(`resolveModelApp: "${recorded.into}" is drawn in no rows or timeline block of "${app.alias}" \u2014 validate it first`);
38202
+ fail2(`resolveModelApp: "${recorded.into}" is drawn in no rows or timeline block of "${app.alias}" \u2014 validate it first`);
36848
38203
  }
36849
- if (entityOf(model, recorded.into) === void 0) throw new Error(`resolveModelApp: "${recorded.into}" is not an entity \u2014 validate it first`);
38204
+ if (entityOf(model, recorded.into) === void 0) fail2(`resolveModelApp: "${recorded.into}" is not an entity \u2014 validate it first`);
36850
38205
  return {
36851
38206
  entity: recorded.into,
36852
38207
  via: block.via,
@@ -36874,13 +38229,13 @@ function actsReadBlocks(model, entity, section, acts) {
36874
38229
  function resolveAct(model, app, act, checks, sections) {
36875
38230
  const alias2 = actEntity(app, act);
36876
38231
  const entity = entityOf(model, alias2);
36877
- if (entity === void 0) throw new Error(`resolveModelApp: act "${act.alias}" works on "${alias2}", which is not an entity \u2014 validate it first`);
38232
+ if (entity === void 0) fail2(`resolveModelApp: act "${act.alias}" works on "${alias2}", which is not an entity \u2014 validate it first`);
36878
38233
  const inputs = (act.asks ?? []).filter(isInput);
36879
38234
  const set2 = Object.fromEntries(
36880
38235
  Object.entries(act.set ?? {}).map(([name, value]) => {
36881
38236
  const field = fieldOf(entity, name);
36882
38237
  const written2 = field === void 0 ? void 0 : actSetValue(field, value, inputs);
36883
- if (written2 === void 0 || "refused" in written2) throw new Error(`resolveModelApp: act "${act.alias}" sets "${name}" in a way the check refuses \u2014 validate it first`);
38238
+ if (written2 === void 0 || "refused" in written2) fail2(`resolveModelApp: act "${act.alias}" sets "${name}" in a way the check refuses \u2014 validate it first`);
36884
38239
  return [name, written2];
36885
38240
  })
36886
38241
  );
@@ -36920,9 +38275,9 @@ function resolveAct(model, app, act, checks, sections) {
36920
38275
  }
36921
38276
  function resolveUnder(model, parent, child, field, where) {
36922
38277
  const shape = documentUnder(model, parent, child, field);
36923
- if ("refused" in shape) throw new Error(`resolveModelApp: the lines of "${child.alias}" ${shape.refused} \u2014 validate it first`);
38278
+ if ("refused" in shape) fail2(`resolveModelApp: the lines of "${child.alias}" ${shape.refused} \u2014 validate it first`);
36924
38279
  const document = entityOf(model, shape.entity);
36925
- if (document === void 0) throw new Error(`resolveModelApp: "${shape.entity}" is not an entity \u2014 validate it first`);
38280
+ if (document === void 0) fail2(`resolveModelApp: "${shape.entity}" is not an entity \u2014 validate it first`);
36926
38281
  const held = where.filter((one) => {
36927
38282
  const own = fieldOf(document, one.field);
36928
38283
  return own !== void 0 && isSingleSelect(own) && one.options.every((option) => own.options.some((declared) => declared.alias === option));
@@ -37077,6 +38432,7 @@ function checkRegisterApp(model, app, at2) {
37077
38432
  findings: [
37078
38433
  ...noteTasks(model, resolved, at2),
37079
38434
  ...checkCreates(ownModel, resolved, at2),
38435
+ ...checkEdits(ownModel, resolved, at2),
37080
38436
  ...noteChildLists(ownModel, app, at2),
37081
38437
  ...noteIntake(ownModel, app, resolved, at2),
37082
38438
  ...checkFirstClass(ownModel, app, resolved, at2),
@@ -37112,6 +38468,28 @@ function checkReadsRows(model, reads, entities, at2) {
37112
38468
  }
37113
38469
  return findings;
37114
38470
  }
38471
+ function checkEdits(model, resolved, at2) {
38472
+ const plan = appWritePlan(model, resolved);
38473
+ const register = resolved.register.remove === false && plan.get(resolved.entity) === void 0 ? [error51("register.remove", `${at2}.register.remove`, `hides a Delete this app never offers on a "${resolved.entity}" record \u2014 it writes no "${resolved.entity}" record here; drop \`remove\``)] : [];
38474
+ return [...register, ...resolved.record.sections.flatMap(
38475
+ (section, index) => section.blocks.flatMap((block, order) => {
38476
+ if (block.kind !== "rows") return [];
38477
+ const saved = new Set(plan.get(block.entity)?.update ?? []);
38478
+ const hidden = block.remove === false && plan.get(block.entity) === void 0 ? [error51("block.remove", `${at2}.record.sections.${index}.blocks.${order}.remove`, `hides a Delete this app never offers on a "${block.entity}" row \u2014 no person writes "${block.entity}" here; drop \`remove\``)] : [];
38479
+ return [...hidden, ...(block.edits ?? []).flatMap(
38480
+ (name, place) => saved.has(name) ? [] : [error51("block.edits", `${at2}.record.sections.${index}.blocks.${order}.edits.${place}`, `writes "${name}" in place, and this app's save of a "${block.entity}" row does not write it \u2014 an act owns it, or no person writes "${block.entity}" here`)]
38481
+ )];
38482
+ })
38483
+ )];
38484
+ }
38485
+ function heldAtStart(model, entity, where) {
38486
+ const declared = entityOf(model, entity);
38487
+ return where.flatMap((gate) => {
38488
+ const field = declared?.fields.find((one) => one.alias === gate.field);
38489
+ const [only, ...more] = gate.options;
38490
+ return field?.type === "select" && more.length === 0 && field.default?.length === 1 && field.default[0] === only ? [gate.field] : [];
38491
+ });
38492
+ }
37115
38493
  function checkCreates(model, resolved, at2) {
37116
38494
  const lists = [
37117
38495
  ...resolved.register.create === void 0 ? [] : [{ entity: resolved.entity, asked: resolved.register.create, fills: resolved.scope === void 0 ? [] : [resolved.scope.field], path: `${at2}.register.create` }],
@@ -37123,8 +38501,9 @@ function checkCreates(model, resolved, at2) {
37123
38501
  {
37124
38502
  entity: block.entity,
37125
38503
  asked: block.create,
37126
- // The link to the record is set by the add; a composer stamps the moment and the author.
37127
- fills: [block.via, ...block.kind === "timeline" ? [block.at, ...block.by === void 0 ? [] : [block.by]] : []],
38504
+ // The link to the record is set by the add; a composer stamps the moment and the author; a select the
38505
+ // block holds at the option its rows start at is where the add's row starts, no statement of its own.
38506
+ fills: [block.via, ...block.kind === "timeline" ? [block.at, ...block.by === void 0 ? [] : [block.by]] : [], ...block.kind === "rows" ? heldAtStart(model, block.entity, block.where) : []],
37128
38507
  path: `${at2}.record.sections.${index}.blocks.${order}.create`
37129
38508
  }
37130
38509
  ] : [],
@@ -37208,45 +38587,16 @@ function checkApp(model, app, at2) {
37208
38587
  }
37209
38588
  const splits = new Set((app.register?.readings ?? []).flatMap((reading) => "metric" in reading ? [] : pressFields(modelPressed(reading))));
37210
38589
  for (const [index, stated] of (app.register?.filters ?? []).entries()) {
37211
- const name = filterField(stated);
37212
38590
  const path15 = `${at2}.register.filters.${index}`;
37213
- const filter2 = own(name, typeof stated === "string" ? path15 : `${path15}.field`);
37214
- const type = filter2 === void 0 ? void 0 : resolvedFieldType(filter2, entity, model.entities) ?? filter2.type;
37215
- if (filter2 !== void 0 && name === display.status?.field) {
37216
- findings.push(error51("register.filter", path15, `"${name}" is the status, already the register's chips \u2014 drop it`));
37217
- } else if (filter2 !== void 0 && (type === void 0 || !FILTER_TYPES.has(type))) {
37218
- const read = filter2.type === "lookup" ? readField(model, entity, filter2) : void 0;
37219
- const kind = read === void 0 ? `a ${filter2.type} field` : `a lookup of a ${read.field.type} field`;
37220
- findings.push(error51("register.filter", path15, `"${name}" is ${kind} \u2014 a register narrows by a select, a member, a link, a date, a yes/no or a number`));
37221
- } else if (filter2 !== void 0 && typeof stated !== "string") {
37222
- findings.push(...checkTiers(model, entity, filter2, type, `${path15}.tiers`));
37223
- } else if (filter2 !== void 0 && splits.has(name) && type !== "select_member") {
37224
- findings.push(note("register.filter-split", path15, `"${name}" is also what a picture above the rows splits them by \u2014 a press on its part narrows by it`));
37225
- }
37226
- scoped(name, path15, "it lists");
38591
+ findings.push(...checkFilter(model, entity, stated, path15, { owner: "a register", chips: display.status?.field, splits }));
38592
+ scoped(filterField(stated), path15, "it lists");
37227
38593
  }
37228
38594
  for (const [index, name] of (app.register?.search ?? []).entries()) {
37229
38595
  const path15 = `${at2}.register.search.${index}`;
37230
- const field = own(name, path15);
37231
- const type = field === void 0 ? void 0 : resolvedFieldType(field, entity, model.entities) ?? field.type;
37232
- if (field !== void 0 && (type === void 0 || !SEARCH_TYPES.has(type))) {
37233
- findings.push(error51("register.search", path15, `"${name}" is a ${field.type} field \u2014 a search matches words: text, a code, or a link by its row's title`));
37234
- }
38596
+ findings.push(...checkSearch(model, entity, name, path15));
37235
38597
  scoped(name, path15, "it lists");
37236
38598
  }
37237
- const checkSort = (sort, base) => {
37238
- const sorted = /* @__PURE__ */ new Set();
37239
- for (const [index, key] of registerSortKeys(sort).entries()) {
37240
- const path15 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
37241
- const field = own(key.field, path15);
37242
- if (field !== void 0 && !isSortableFieldType(field.type)) {
37243
- findings.push(error51("register.sort", path15, `"${key.field}" is a ${field.type} field, which holds no order`));
37244
- }
37245
- if (sorted.has(key.field)) findings.push(error51("register.sort", path15, `"${key.field}" is already a key of this order \u2014 a field orders the rows once`));
37246
- sorted.add(key.field);
37247
- }
37248
- };
37249
- checkSort(app.register?.sort, `${at2}.register.sort`);
38599
+ findings.push(...checkOrder(entity, app.register?.sort, `${at2}.register.sort`));
37250
38600
  const tabs = app.register?.tabs ?? [];
37251
38601
  const dated = (name, path15) => {
37252
38602
  const field = own(name, path15);
@@ -37279,11 +38629,17 @@ function checkApp(model, app, at2) {
37279
38629
  if (status !== void 0 && tab.where?.[status] !== void 0) {
37280
38630
  findings.push(error51("register.tabs", `${path15}.where.${status}`, `narrows by "${status}", the status \u2014 its chips split the rows by it, each with its count; a tab splits them by the period or another field`));
37281
38631
  }
37282
- checkSort(tab.sort, `${path15}.sort`);
38632
+ findings.push(...checkOrder(entity, tab.sort, `${path15}.sort`));
38633
+ }
38634
+ const group = app.register?.group;
38635
+ if (group !== void 0 && group === app.scope) {
38636
+ findings.push(error51("app.scope", `${at2}.register.group`, `"${group}" is the folder the app is inside, one group of every row \u2014 group by another field`));
38637
+ } else if (group !== void 0) {
38638
+ findings.push(...checkGroup(model, entity, display, group, `${at2}.register.group`, { chips: display.status?.field, layout: app.register?.layout ?? "table" }));
37283
38639
  }
37284
- if (app.register?.group !== void 0) findings.push(...checkGroup(model, entity, display, app, app.register.group, `${at2}.register.group`));
37285
38640
  findings.push(...checkRoster(model, entity, app, at2));
37286
38641
  findings.push(...checkLanes(model, entity, app, at2));
38642
+ findings.push(...checkGantt(model, entity, app, at2));
37287
38643
  findings.push(...checkRegisterNarrowing(model, entity, app, at2));
37288
38644
  findings.push(...checkFiltersShown(model, entity, app, at2));
37289
38645
  const create = app.register?.create;
@@ -37336,6 +38692,7 @@ function checkApp(model, app, at2) {
37336
38692
  if (scope !== void 0 && !placed.has(scope)) place(scope, "the header's folder", `${at2}.record`);
37337
38693
  const sections = app.record?.sections ?? [];
37338
38694
  findings.push(...checkPages(app.record?.door, sections, `${at2}.record`));
38695
+ findings.push(...checkRecordLayout(app, `${at2}.record`));
37339
38696
  if (app.record?.history === true && display.status?.history === void 0) {
37340
38697
  findings.push(error51("record.history", `${at2}.record.history`, `draws the status history of "${entity.alias}", which records none \u2014 name the history entity in \`records.${entity.alias}.status.history\`, or drop it`));
37341
38698
  }
@@ -37388,13 +38745,20 @@ function checkApp(model, app, at2) {
37388
38745
  const first2 = identity === void 0 ? void 0 : blocksPlaced.get(identity);
37389
38746
  if (first2 !== void 0) findings.push(error51("record.placed-once", path15, `places the same block a second time on the record (it is already in ${first2}) \u2014 a block is read once`));
37390
38747
  else if (identity !== void 0) blocksPlaced.set(identity, `the section "${section.title}"`);
37391
- findings.push(...checkBlock(model, entity, block, path15, place, app.reads !== "shared"));
38748
+ findings.push(...checkBlock(model, entity, block, path15, place, app.reads !== "shared", app.checks ?? []));
37392
38749
  if (!isRowsBlock(block)) continue;
37393
38750
  const child = entityOf(model, block.rows);
37394
38751
  const link = child === void 0 ? void 0 : childLink(child, entity.alias, block.via);
37395
38752
  if (child === void 0 || link === void 0 || !("link" in link)) continue;
37396
- const where = Object.entries(block.where ?? {}).map(([field, options]) => ({ field, options }));
37397
- for (const total of blockTotals(model, entity.alias, child.alias, link.link, block.columns ?? [], where).values()) {
38753
+ const whereOf2 = (one) => Object.entries(one.where ?? {}).map(([field, options]) => ({ field, options }));
38754
+ const siblings = sections.flatMap(
38755
+ (other) => (other.blocks ?? []).flatMap((one) => {
38756
+ if (!isRowsBlock(one) || one.rows !== block.rows) return [];
38757
+ const theirs = childLink(child, entity.alias, one.via);
38758
+ return theirs !== void 0 && "link" in theirs && theirs.link === link.link ? [whereOf2(one)] : [];
38759
+ })
38760
+ );
38761
+ for (const total of blockTotals(model, entity.alias, child.alias, link.link, block.columns ?? [], whereOf2(block), siblings).values()) {
37398
38762
  const inFact = inFacts.get(total);
37399
38763
  if (inFact !== void 0) findings.push(error51("record.foot-total", inFact, `"${total}" is already the total at the foot of the "${block.title ?? child.label}" block \u2014 drop it from the section`));
37400
38764
  }
@@ -37414,14 +38778,8 @@ function checkApp(model, app, at2) {
37414
38778
  if (act.of !== void 0 && act.on === "view") {
37415
38779
  findings.push(error51("act.of", `${path15}.on`, `is "${act.on}", and an act of a child works on the one child row it is pressed on \u2014 drop it`));
37416
38780
  }
37417
- if (act.of !== void 0 && act.on === "rows" && !listsTasks(model, app, act.of)) {
37418
- findings.push(
37419
- error51(
37420
- "act.of",
37421
- `${path15}.on`,
37422
- `is "rows", and "${act.of}" rows are picked together only where the record lists them as tasks \u2014 \`records.${act.of}.task\` over a stored status select with \`closed\`, in a rows block without \`expect\` or \`under\``
37423
- )
37424
- );
38781
+ if (act.of !== void 0 && act.on === "rows" && !listsRows(app, act.of)) {
38782
+ findings.push(error51("act.of", `${path15}.on`, `is "rows", and the record lists no "${act.of}" rows to pick together \u2014 list them in a rows block without \`expect\``));
37425
38783
  }
37426
38784
  const on = entityOf(model, actEntity(app, act));
37427
38785
  if (on === void 0) continue;
@@ -37566,7 +38924,43 @@ function hiddenWhenOffered(shown, offered) {
37566
38924
  return !held.some((option) => options.includes(option));
37567
38925
  });
37568
38926
  }
37569
- function checkGroup(model, entity, display, app, name, path15) {
38927
+ function checkFilter(model, entity, stated, path15, around) {
38928
+ const name = filterField(stated);
38929
+ const filter2 = fieldOf(entity, name);
38930
+ if (filter2 === void 0) return [error51("model.names-declared", typeof stated === "string" ? path15 : `${path15}.field`, `names "${name}", which is not a field of "${entity.alias}"`)];
38931
+ const type = resolvedFieldType(filter2, entity, model.entities) ?? filter2.type;
38932
+ if (name === around.chips) return [error51("register.filter", path15, `"${name}" is the status, already the register's chips \u2014 drop it`)];
38933
+ if (!FILTER_TYPES.has(type)) {
38934
+ const read = filter2.type === "lookup" ? readField(model, entity, filter2) : void 0;
38935
+ const kind = read === void 0 ? `a ${filter2.type} field` : `a lookup of a ${read.field.type} field`;
38936
+ return [error51("register.filter", path15, `"${name}" is ${kind} \u2014 ${around.owner} narrows by a select, a member, a link, a date, a yes/no or a number`)];
38937
+ }
38938
+ if (typeof stated !== "string") return checkTiers(model, entity, filter2, type, `${path15}.tiers`);
38939
+ if (around.splits.has(name) && type !== "select_member") {
38940
+ return [note("register.filter-split", path15, `"${name}" is also what a picture above the rows splits them by \u2014 a press on its part narrows by it`)];
38941
+ }
38942
+ return [];
38943
+ }
38944
+ function checkOrder(entity, sort, base) {
38945
+ const findings = [];
38946
+ const sorted = /* @__PURE__ */ new Set();
38947
+ for (const [index, key] of registerSortKeys(sort).entries()) {
38948
+ const path15 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
38949
+ const field = fieldOf(entity, key.field);
38950
+ if (field === void 0) findings.push(error51("model.names-declared", path15, `names "${key.field}", which is not a field of "${entity.alias}"`));
38951
+ else if (!isSortableFieldType(field.type)) findings.push(error51("register.sort", path15, `"${key.field}" is a ${field.type} field, which holds no order`));
38952
+ if (sorted.has(key.field)) findings.push(error51("register.sort", path15, `"${key.field}" is already a key of this order \u2014 a field orders the rows once`));
38953
+ sorted.add(key.field);
38954
+ }
38955
+ return findings;
38956
+ }
38957
+ function checkSearch(model, entity, name, path15) {
38958
+ const field = fieldOf(entity, name);
38959
+ if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
38960
+ const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
38961
+ return SEARCH_TYPES.has(type) ? [] : [error51("register.search", path15, `"${name}" is a ${field.type} field \u2014 a search matches words: text, a code, or a link by its row's title`)];
38962
+ }
38963
+ function checkGroup(model, entity, display, name, path15, around) {
37570
38964
  const field = fieldOf(entity, name);
37571
38965
  if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
37572
38966
  const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
@@ -37577,9 +38971,8 @@ function checkGroup(model, entity, display, app, name, path15) {
37577
38971
  if (several || many || !GROUP_TYPES.has(type)) {
37578
38972
  return [error51("register.group", path15, `"${name}" is a ${several || many ? `${field.type} field holding several values` : `${field.type} field`} \u2014 a row stands in one group: a single select, a one-row link, one member, a date, a yes/no or text`)];
37579
38973
  }
37580
- if (name === display.status?.field) return [error51("register.group", path15, `"${name}" is the status, whose chips already split the rows \u2014 group by another field`)];
37581
- if (name === app.scope) return [error51("app.scope", path15, `"${name}" is the folder the app is inside, one group of every row \u2014 group by another field`)];
37582
- if ((app.register?.layout ?? "table") !== "table") return [error51("register.group", path15, `groups a ${app.register?.layout ?? ""} register \u2014 rows are grouped in a table`)];
38974
+ if (name === around.chips) return [error51("register.group", path15, `"${name}" is the status, whose chips already split the rows \u2014 group by another field`)];
38975
+ if (around.layout !== "table" && around.layout !== "gantt") return [error51("register.group", path15, `groups a ${around.layout} register \u2014 rows are grouped in a table, and a gantt's lanes`)];
37583
38976
  if (name === display.title && (display.subtitle ?? []).length === 0) {
37584
38977
  return [error51("register.group", path15, `groups by "${name}", the row's title, so each row leads with its first subtitle \u2014 state \`records.${entity.alias}.subtitle\` (the person, the party)`)];
37585
38978
  }
@@ -37621,7 +39014,10 @@ function checkPages(door, sections, at2) {
37621
39014
  for (const [index, section] of sections.entries()) {
37622
39015
  if (section.page !== true) continue;
37623
39016
  const path15 = `${at2}.sections.${index}.page`;
37624
- if (door === "drawer") findings.push(error51("record.page", path15, `"${section.title}" is a page of its own, and a drawer has no pages; drop \`page\`, or make the door a page`));
39017
+ if (door === "drawer" || door === "beside") {
39018
+ const has = door === "drawer" ? "a drawer" : "a record beside the list";
39019
+ findings.push(error51("record.page", path15, `"${section.title}" is a page of its own, and ${has} has no pages; drop \`page\`, or make the door a page`));
39020
+ }
37625
39021
  if (section.at !== void 0) findings.push(error51("record.page", path15, `"${section.title}" is a step of the record's path, read in its scroll \u2014 drop \`page\`, or \`at\``));
37626
39022
  const segment = pageSegment(section.title);
37627
39023
  const first2 = addressed.get(segment);
@@ -37634,6 +39030,25 @@ function checkPages(door, sections, at2) {
37634
39030
  }
37635
39031
  return findings;
37636
39032
  }
39033
+ function checkRecordLayout(app, at2) {
39034
+ const record2 = app.record;
39035
+ const findings = [];
39036
+ const sides = (record2?.sections ?? []).flatMap((section, index) => section.side === true ? [{ section, index }] : []);
39037
+ if (sides.length > 0 && record2?.door !== void 0 && record2.door !== "page") {
39038
+ findings.push(error51("record.side", `${at2}.door`, `"${sides[0]?.section.title ?? ""}" stands beside the rest, and a ${record2.door} has no room beside its sections \u2014 drop \`door: "${record2.door}"\`, or \`side\``));
39039
+ }
39040
+ for (const { section, index } of sides.slice(1)) {
39041
+ findings.push(error51("record.side", `${at2}.sections.${index}.side`, `"${section.title}" stands beside the rest, as "${sides[0]?.section.title ?? ""}" already does \u2014 one section stands beside the others`));
39042
+ }
39043
+ for (const { section, index } of sides) {
39044
+ if (section.page === true) findings.push(error51("record.side", `${at2}.sections.${index}`, `"${section.title}" stands beside the rest and is a page of its own \u2014 it is one or the other`));
39045
+ }
39046
+ const layout = app.register?.layout ?? "table";
39047
+ if (record2?.door === "beside" && layout !== "table") {
39048
+ findings.push(error51("record.beside", `${at2}.door`, `opens the record beside the register's list, and the register is \`${layout}\`, which draws no list \u2014 drop \`door: "beside"\`, or the layout`));
39049
+ }
39050
+ return findings;
39051
+ }
37637
39052
  function checkSectionAt(entity, display, at2, path15) {
37638
39053
  if (at2 === void 0) return [];
37639
39054
  const status = display.status;
@@ -37828,6 +39243,17 @@ function checkLanes(model, entity, app, at2) {
37828
39243
  if (count2 === 0 || count2 > LANE_LOADS) return [error51("register.loads", `${at2}.register.loads`, `names ${count2} loads \u2014 a lane reads one or ${LANE_LOADS}, the ones it runs out of first (its weight, its volume)`)];
37829
39244
  return laneLoads(model, entity.alias, shape, loads).refused.map((one) => error51("register.loads", `${at2}.register.loads.${one.value}`, one.reason));
37830
39245
  }
39246
+ function checkGantt(model, entity, app, at2) {
39247
+ const layout = app.register?.layout;
39248
+ const gantt = app.register?.gantt;
39249
+ const path15 = `${at2}.register.gantt`;
39250
+ if (gantt === void 0) {
39251
+ return layout === "gantt" ? [error51("register.gantt", path15, "a gantt draws each row as a bar between two dates \u2014 name them in `gantt` (`start`, and `end` where the entity states no `due`)")] : [];
39252
+ }
39253
+ if (layout !== "gantt") return [error51("register.gantt", path15, `names the dates a gantt's bars run between, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "gantt"\`, or drop it`)];
39254
+ const shape = ganttShape(model, entity.alias, gantt);
39255
+ return "refused" in shape ? shape.refused.map((one) => error51("register.gantt", one.at === "" ? path15 : `${path15}.${one.at}`, one.reason)) : [];
39256
+ }
37831
39257
  function checkTiers(model, entity, field, type, path15) {
37832
39258
  if (type !== "number") return [error51("register.tiers", path15, `"${field.alias}" is a ${field.type} field \u2014 tiers band a number; name it bare`)];
37833
39259
  const select = contractFigureSelect(field, entity, model.entities);
@@ -37893,24 +39319,13 @@ function checkFiltersShown(model, entity, app, at2) {
37893
39319
  const register = app.register;
37894
39320
  const display = recordDisplay(withAppRow(model, app), entity.alias);
37895
39321
  if (register === void 0 || display === void 0) return [];
37896
- const columns = register.columns ?? [];
37897
- const drawn = [...partsOf(display).keys(), ...meterBounds(display, columns).keys(), ...columns];
37898
- const shown = /* @__PURE__ */ new Set([
37899
- ...drawn,
37900
- // A figure read in its row's own unit or currency says that select beside its number.
37901
- ...drawn.flatMap((name) => {
37902
- const field = fieldOf(entity, name);
37903
- const select = field === void 0 ? void 0 : contractFigureSelect(field, entity, model.entities);
37904
- return select === void 0 ? [] : [select.alias];
37905
- }),
37906
- ...display.due ?? [],
39322
+ const shown = rowShows(model, entity, display, register.columns ?? [], [
37907
39323
  // A guard waits at the act it refuses; a register's row says its warnings.
37908
39324
  ...(app.checks ?? []).flatMap((check2) => check2.of === void 0 && check2.blocks === void 0 ? [check2.field] : []),
37909
39325
  ...register.group === void 0 ? [] : [register.group],
37910
39326
  ...register.lanes === void 0 ? [] : [register.lanes]
37911
39327
  ]);
37912
39328
  const fixed = /* @__PURE__ */ new Set([...heldBy(register.where).flatMap((held) => held.values === 1 ? [held.field] : []), ...app.scope === void 0 ? [] : [app.scope]]);
37913
- const unshown = (name) => `"${name}" is drawn nowhere on the rows it narrows \u2014 make it a column or part of the row's line, or drop it`;
37914
39329
  const pressed = (part) => (name) => `a press on its ${part} narrows the rows by "${name}", which is drawn nowhere on them \u2014 make it a column or part of the row's line, or drop the reading`;
37915
39330
  const narrowed = [
37916
39331
  ...(register.filters ?? []).map((stated, index) => ({ name: filterField(stated), path: `${at2}.register.filters.${index}`, says: unshown })),
@@ -37922,6 +39337,23 @@ function checkFiltersShown(model, entity, app, at2) {
37922
39337
  return pressFields(modelPressed(reading)).map((name) => ({ name, path: path15, says: pressed(part) }));
37923
39338
  })
37924
39339
  ];
39340
+ return unshownFilters(model, entity, narrowed, shown, fixed);
39341
+ }
39342
+ var unshown = (name) => `"${name}" is drawn nowhere on the rows it narrows \u2014 make it a column or part of the row's line, or drop it`;
39343
+ function rowShows(model, entity, display, columns, more) {
39344
+ const drawn = [...partsOf(display).keys(), ...meterBounds(display, columns).keys(), ...columns];
39345
+ return /* @__PURE__ */ new Set([
39346
+ ...drawn,
39347
+ ...drawn.flatMap((name) => {
39348
+ const field = fieldOf(entity, name);
39349
+ const select = field === void 0 ? void 0 : contractFigureSelect(field, entity, model.entities);
39350
+ return select === void 0 ? [] : [select.alias];
39351
+ }),
39352
+ ...display?.due ?? [],
39353
+ ...more
39354
+ ]);
39355
+ }
39356
+ function unshownFilters(model, entity, narrowed, shown, fixed) {
37925
39357
  return narrowed.flatMap(({ name, path: path15, says }) => {
37926
39358
  const field = fieldOf(entity, name);
37927
39359
  const type = field === void 0 ? void 0 : resolvedFieldType(field, entity, model.entities) ?? field.type;
@@ -37929,7 +39361,7 @@ function checkFiltersShown(model, entity, app, at2) {
37929
39361
  return [error51("register.filter-unshown", path15, says(name))];
37930
39362
  });
37931
39363
  }
37932
- function checkBlock(model, entity, block, at2, place, scoped) {
39364
+ function checkBlock(model, entity, block, at2, place, scoped, checks) {
37933
39365
  const findings = [];
37934
39366
  const unruled = (child, through) => scoped ? unruledChild(model, entity, child, through, at2) : [];
37935
39367
  if (isReadingBlock(block)) {
@@ -37987,6 +39419,8 @@ function checkBlock(model, entity, block, at2, place, scoped) {
37987
39419
  if (block.expect !== void 0) findings.push(...checkExpect(model, child, block, back, `${at2}.expect`));
37988
39420
  if (block.of !== void 0) findings.push(...checkExpectOf(entity, child, block, `${at2}.of`));
37989
39421
  if (block.under !== void 0) findings.push(...checkUnder(model, entity, child, block, at2));
39422
+ findings.push(...checkBlockNarrowing(model, child, block, back, checks, at2));
39423
+ findings.push(...checkBlockEdits(model, child, block, at2));
37990
39424
  } else if (!child.fields.some((field) => resolvedFieldType(field, child, model.entities) === "date")) {
37991
39425
  findings.push(error51("block.timeline", at2, `reads "${child.alias}" as a timeline, and it has no date to place its entries by`));
37992
39426
  }
@@ -38011,6 +39445,59 @@ function checkBlock(model, entity, block, at2, place, scoped) {
38011
39445
  }
38012
39446
  return findings;
38013
39447
  }
39448
+ function checkBlockNarrowing(model, child, block, back, checks, at2) {
39449
+ const display = recordDisplay(model, child.alias);
39450
+ const filters = block.filters ?? [];
39451
+ if (display === void 0 || filters.length === 0 && block.search === void 0 && block.group === void 0 && block.sort === void 0) return [];
39452
+ const findings = [];
39453
+ if (block.expect !== void 0) {
39454
+ findings.push(error51("block.narrow", at2, `expects a line per "${block.expect}", each drawn filled or not in its key's order \u2014 a narrowing would hide one, an order of its own shuffle them; drop \`filters\`, \`search\`, \`group\` and \`sort\``));
39455
+ }
39456
+ const chips = isTaskBlock(model, block) ? display.status?.field : void 0;
39457
+ const fixed = /* @__PURE__ */ new Set([...back === void 0 ? [] : [back], ...Object.entries(block.where ?? {}).flatMap(([field, options]) => options.length === 1 ? [field] : [])]);
39458
+ const same = (name, path15) => fixed.has(name) ? [error51("block.narrow", path15, `"${name}" is ${name === back ? "the link back to this record" : "held at one option by the block's `where`"}, the same on every row it narrows \u2014 drop it here`)] : [];
39459
+ for (const [index, stated] of filters.entries()) {
39460
+ const path15 = `${at2}.filters.${index}`;
39461
+ findings.push(...checkFilter(model, child, stated, path15, { owner: "a rows block", chips, splits: /* @__PURE__ */ new Set() }), ...same(filterField(stated), path15));
39462
+ }
39463
+ for (const [index, name] of (block.search ?? []).entries()) findings.push(...checkSearch(model, child, name, `${at2}.search.${index}`));
39464
+ findings.push(...checkOrder(child, block.sort, `${at2}.sort`));
39465
+ if (block.group !== void 0) {
39466
+ const path15 = `${at2}.group`;
39467
+ const fixedGroup = same(block.group, path15);
39468
+ if (block.under !== void 0) findings.push(error51("block.narrow", path15, `stands its lines under their documents ("${block.under}"), which already group them \u2014 drop \`group\``));
39469
+ else if (fixedGroup.length > 0) findings.push(...fixedGroup);
39470
+ else findings.push(...checkGroup(model, child, display, block.group, path15, { chips, layout: "table" }));
39471
+ }
39472
+ const shown = rowShows(model, child, display, block.columns ?? [], [
39473
+ // A child row's line says the warnings standing on it.
39474
+ ...checks.flatMap((check2) => check2.of === child.alias && check2.blocks === void 0 ? [check2.field] : []),
39475
+ ...block.group === void 0 ? [] : [block.group]
39476
+ ]);
39477
+ const narrowed = filters.map((stated, index) => ({ name: filterField(stated), path: `${at2}.filters.${index}`, says: unshown }));
39478
+ return [...findings, ...unshownFilters(model, child, narrowed, shown, fixed)];
39479
+ }
39480
+ var LINE_EDIT_TYPES = /* @__PURE__ */ new Set(["number", "date", "text", "select"]);
39481
+ function checkBlockEdits(model, child, block, at2) {
39482
+ const edits = block.edits ?? [];
39483
+ if (edits.length === 0) return [];
39484
+ if (block.expect !== void 0) return [error51("block.edits", `${at2}.edits`, `expects a line per "${block.expect}", whose columns a person fills in place already \u2014 drop \`edits\``)];
39485
+ const figure = recordDisplay(model, child.alias)?.figure;
39486
+ const shown = /* @__PURE__ */ new Set([...block.columns ?? [], ...figure === void 0 ? [] : [figure]]);
39487
+ return edits.flatMap((name, index) => {
39488
+ const path15 = `${at2}.edits.${index}`;
39489
+ const field = fieldOf(child, name);
39490
+ if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${child.alias}"`)];
39491
+ if (!shown.has(name)) return [error51("block.edits", path15, `"${name}" is drawn nowhere on the line \u2014 make it a column, or drop it here`)];
39492
+ if (!isAsked(field)) return [error51("block.edits", path15, `"${name}" is a ${field.type} field nobody writes \u2014 a line writes a value a person keys`)];
39493
+ const long = field.type === "text" && field.format === "markdown";
39494
+ const several = field.type === "select" && field.multi === true;
39495
+ if (!LINE_EDIT_TYPES.has(field.type) || long || several) {
39496
+ return [error51("block.edits", path15, `"${name}" is a ${long ? "long text" : several ? "multi-select" : `${field.type}`} field \u2014 a line holds a number, a date, a few words or one option as its control; the row opens to write the rest`)];
39497
+ }
39498
+ return [];
39499
+ });
39500
+ }
38014
39501
  function checkExpectOf(entity, child, block, at2) {
38015
39502
  const name = block.of;
38016
39503
  if (name === void 0) return [];
@@ -38071,269 +39558,6 @@ function fileLed(model, child) {
38071
39558
  return field?.type === "files" && isAsked(field) ? image : void 0;
38072
39559
  }
38073
39560
 
38074
- // ../shared/src/schemas/write_rules.ts
38075
- var BOUND_REF = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
38076
- var BOUND_REF_MESSAGE = 'a bound is a number, "<field alias>" or "<link alias>.<field alias>"';
38077
- var fieldWriteRuleSchema = zod_default.object({
38078
- default_from: zod_default.string().regex(/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/, 'default_from is "<link alias>.<field alias>"').optional().describe(
38079
- "Copy this field's value from the linked row at CREATE time \u2014 `<link alias>.<field alias>`, the link being a one-row link on this entity, required or filled by the add (the record a row is added under). The value is copied rather than looked up, so the source changing later leaves the row alone."
38080
- ),
38081
- min: zod_default.union([zod_default.number(), zod_default.string().regex(BOUND_REF, BOUND_REF_MESSAGE)]).optional().describe(
38082
- "Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line's day inside its trip). On a figure whose unit or currency is per row (`unit_field`, `currency_field`) a constant bound is only 0; bound it by a field of the row"
38083
- ),
38084
- max: zod_default.union([zod_default.number(), zod_default.string().regex(BOUND_REF, BOUND_REF_MESSAGE)]).optional().describe("Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names"),
38085
- options_where: tableRecordFiltersSchema.optional().describe(
38086
- "Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity's own fields, each `field_key` a field alias. It narrows the picker's read and is refused again where the write lands."
38087
- ),
38088
- same: zod_default.array(contractAliasSchema).min(1).optional().describe(
38089
- "One-row links this row and each row this link points at must name alike \u2014 a fee's invoice is one of its own visit's, a box's seal one of its own shipping line's: each a one-row link of both entities to the same entity. It narrows the picker's read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare."
38090
- )
38091
- }).strict();
38092
- var entityWriteRulesSchema = zod_default.object({
38093
- natural_key: zod_default.array(contractAliasSchema).min(1).optional().describe(
38094
- "The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches."
38095
- ),
38096
- fields: zod_default.record(contractAliasSchema, fieldWriteRuleSchema).optional().describe("Field alias \u2192 what that field's value is copied from, bounded by or picked among"),
38097
- no_overlap: zod_default.object({
38098
- from: contractAliasSchema.describe("The stored date (or datetime) a row's span starts on"),
38099
- to: contractAliasSchema.describe(
38100
- "The stored date (or datetime) a row's span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at"
38101
- ),
38102
- by: zod_default.array(contractAliasSchema).min(1).optional().describe("Fields two rows share to compete for a span (the same employee, the same room); absent, every row"),
38103
- while: zod_default.record(contractAliasSchema, zod_default.array(contractAliasSchema).min(1)).optional().describe("Only rows at these options of these selects hold their span (running, not closed); absent, every row")
38104
- }).strict().optional().describe("No two rows hold overlapping spans \u2014 a create or update that would is refused")
38105
- }).strict();
38106
- var writeRulesSchema = zod_default.record(contractAliasSchema, entityWriteRulesSchema);
38107
- function boundRef(bound) {
38108
- if (typeof bound !== "string") return void 0;
38109
- const [first2 = "", second] = bound.split(".");
38110
- return second === void 0 ? { field: first2 } : { link: first2, field: second };
38111
- }
38112
- var BOUNDED_TYPES = ["number", "date"];
38113
- var NATURAL_KEY_TYPES = ["text", "number", "select_record_link"];
38114
- var OPTIONS_WHERE_OPERATORS = {
38115
- number: ["equals", "not_equals", "greater_than", "less_than", "greater_than_or_equal_to", "less_than_or_equal_to"],
38116
- text: ["equals", "not_equals"],
38117
- boolean: ["equals"],
38118
- select: ["has_any_of", "has_none_of"]
38119
- };
38120
- function optionsWhereConditions(filter2, target) {
38121
- if (filter2.node_type !== "group" || filter2.logic !== "and") return void 0;
38122
- const out = [];
38123
- for (const child of filter2.children) {
38124
- if (child.node_type !== "condition") return void 0;
38125
- const field = target.fields.find((one) => one.alias === child.field_key);
38126
- if (field === void 0) return void 0;
38127
- const allowed = OPTIONS_WHERE_OPERATORS[child.type ?? ""];
38128
- if (allowed === void 0 || !allowed.includes(child.operator)) return void 0;
38129
- out.push({ type: child.type ?? "", field, operator: child.operator, value: child.value });
38130
- }
38131
- return out.length === 0 ? void 0 : out;
38132
- }
38133
- function checkBoundRef(entity, field, ref, byAlias, path15) {
38134
- const entities = [...byAlias.values()];
38135
- let on = entity;
38136
- if ("link" in ref) {
38137
- const link = entity.fields.find((one) => one.alias === ref.link);
38138
- if (link === void 0 || link.type !== "select_record_link" || link.cardinality !== "one") {
38139
- return [error51("write.bounds", path15, `reads "${ref.link}", which is not a one-row link of ${entity.label} \u2014 a bound is read off this row or the one row a link names`)];
38140
- }
38141
- const target = byAlias.get(link.target_entity);
38142
- if (target === void 0) return [error51("model.names-declared", path15, `reads "${ref.link}", whose entity "${link.target_entity}" this model does not declare`)];
38143
- on = target;
38144
- }
38145
- const source = on.fields.find((one) => one.alias === ref.field);
38146
- if (source === void 0) return [error51("model.names-declared", path15, `names "${ref.field}", which is not a field of ${on.label}`)];
38147
- if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds", path15, `bounds ${field.label} by itself`)];
38148
- const from = resolvedFieldType(source, on, entities);
38149
- const into = resolvedFieldType(field, entity, entities);
38150
- if (from !== into) return [error51("write.bounds", path15, `bounds ${field.label} (${String(into)}) by "${source.label}" (${String(from)}) \u2014 a bound is of the value's own type`)];
38151
- const apart = boundUnitRefusal(field, source, entity, entities, "link" in ref ? { link: ref.link, on } : void 0);
38152
- return apart === void 0 ? [] : [error51("write.bounds", path15, apart)];
38153
- }
38154
- function checkSame(entity, field, same, byAlias, path15) {
38155
- if (field.type !== "select_record_link") {
38156
- return [error51("write.same", path15, `${field.label} is a ${field.type} field \u2014 \`same\` narrows the rows a LINK points at`)];
38157
- }
38158
- const target = byAlias.get(field.target_entity);
38159
- if (target === void 0) return [];
38160
- const oneLink = (on, alias2) => on.fields.find((one) => one.alias === alias2 && one.type === "select_record_link" && one.cardinality === "one");
38161
- return same.flatMap((alias2, index) => {
38162
- const own = oneLink(entity, alias2);
38163
- const theirs = oneLink(target, alias2);
38164
- const at2 = `${path15}.${index}`;
38165
- if (own === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${entity.label}`)];
38166
- if (theirs === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${target.label} \u2014 the row ${field.label} points at names nothing to compare`)];
38167
- if (own.type !== "select_record_link" || theirs.type !== "select_record_link" || own.target_entity !== theirs.target_entity) {
38168
- return [error51("write.same", at2, `"${alias2}" names rows of different entities on ${entity.label} and ${target.label}`)];
38169
- }
38170
- return [];
38171
- });
38172
- }
38173
- function checkNoOverlap(entity, rule, at2) {
38174
- const findings = [];
38175
- const own = (name) => entity.fields.find((one) => one.alias === name);
38176
- for (const side of ["from", "to"]) {
38177
- const date5 = own(rule[side]);
38178
- if (date5?.type !== "date") findings.push(error51("write.no-overlap", `${at2}.${side}`, `"${rule[side]}" is not a date field of ${entity.label} \u2014 a span runs between two dates`));
38179
- }
38180
- for (const [index, name] of (rule.by ?? []).entries()) {
38181
- const by = own(name);
38182
- if (by === void 0 || !["select_record_link", "select_member", "select", "text", "number"].includes(by.type) || "multi" in by && by.multi === true) {
38183
- findings.push(error51("write.no-overlap", `${at2}.by.${index}`, `"${name}" is not one value rows can share \u2014 a link, a person, one option, a text or a number`));
38184
- }
38185
- }
38186
- for (const [name, options] of Object.entries(rule.while ?? {})) {
38187
- const select = own(name);
38188
- if (select?.type !== "select" || select.multi === true) {
38189
- findings.push(error51("write.no-overlap", `${at2}.while.${name}`, `"${name}" is not a single select of ${entity.label}`));
38190
- continue;
38191
- }
38192
- for (const option of options) {
38193
- if (!select.options.some((one) => one.alias === option)) findings.push(error51("model.names-declared", `${at2}.while.${name}`, `names option "${option}", which "${select.label}" does not declare`));
38194
- }
38195
- }
38196
- return findings;
38197
- }
38198
- function checkWriteRules(entities, rules) {
38199
- const findings = [];
38200
- const byAlias = new Map(entities.map((entity) => [entity.alias, entity]));
38201
- for (const [alias2, entry] of Object.entries(rules)) {
38202
- const at2 = `write_rules.${alias2}`;
38203
- const entity = byAlias.get(alias2);
38204
- if (entity === void 0) {
38205
- findings.push(error51("model.names-declared", at2, `names entity "${alias2}", which this model does not declare \u2014 write rules are keyed by entity alias (${byAlias.size > 0 ? [...byAlias.keys()].join(", ") : "this model declares none"})`));
38206
- continue;
38207
- }
38208
- const fieldByAlias = new Map(entity.fields.map((field) => [field.alias, field]));
38209
- if (entry.no_overlap !== void 0) findings.push(...checkNoOverlap(entity, entry.no_overlap, `${at2}.no_overlap`));
38210
- const keyed = new Set(entry.natural_key ?? []);
38211
- const uniqueAsSet = (entity.unique ?? []).some((set2) => set2.every((name) => keyed.has(name)));
38212
- for (const [index, key] of (entry.natural_key ?? []).entries()) {
38213
- const field = fieldByAlias.get(key);
38214
- if (field === void 0) {
38215
- findings.push(error51("model.names-declared", `${at2}.natural_key.${index}`, `names "${key}", which is not a field of ${entity.label}`));
38216
- continue;
38217
- }
38218
- if (!NATURAL_KEY_TYPES.includes(field.type)) {
38219
- findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} is a ${field.type} field \u2014 a row is recognised by a value a person types back or the one row it belongs with, so a natural key is text, a number or a one-row link`));
38220
- continue;
38221
- }
38222
- if (field.type === "select_record_link" && field.cardinality !== "one") {
38223
- findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} holds MANY rows \u2014 a row is recognised by the one row it belongs with, so a link in a natural key is of cardinality "one"`));
38224
- continue;
38225
- }
38226
- if (field.type === "text" && field.unique !== true && !uniqueAsSet) {
38227
- findings.push(error51("write.natural-key", `${at2}.natural_key.${index}`, `${field.label} recognises a row and nothing makes it unique \u2014 declare \`unique: true\` on it, or the key's fields as a set in the entity's \`unique\`; two rows sharing it would make find-or-create pick whichever the read answered first`));
38228
- }
38229
- }
38230
- for (const [fieldAlias2, rule] of Object.entries(entry.fields ?? {})) {
38231
- const path15 = `${at2}.fields.${fieldAlias2}`;
38232
- const field = fieldByAlias.get(fieldAlias2);
38233
- if (field === void 0) {
38234
- findings.push(error51("model.names-declared", path15, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
38235
- continue;
38236
- }
38237
- if (rule.default_from !== void 0) {
38238
- findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path15}.default_from`));
38239
- }
38240
- const bounded = resolvedFieldType(field, entity, entities);
38241
- if ((rule.min !== void 0 || rule.max !== void 0) && !BOUNDED_TYPES.includes(bounded ?? field.type)) {
38242
- findings.push(error51("write.bounds", path15, `${field.label} is a ${field.type} field \u2014 \`min\`/\`max\` bound a number or a date`));
38243
- }
38244
- for (const side of ["min", "max"]) {
38245
- const bound = rule[side];
38246
- if (typeof bound === "number" && bounded === "date") {
38247
- findings.push(error51("write.bounds", `${path15}.${side}`, `bounds the date ${field.label} by a number \u2014 a date is bounded by a date field`));
38248
- }
38249
- const perRow = typeof bound === "number" && bound !== 0 ? contractUnitSource(field, entity, entities) : void 0;
38250
- if (perRow !== void 0) {
38251
- findings.push(error51("write.bound-per-row-unit", `${path15}.${side}`, `bounds ${field.label} by ${String(bound)}, which is in no ${perRow.vocabulary}, and each row reads it in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds \u2014 only 0 is the same in every ${perRow.vocabulary}`));
38252
- }
38253
- const ref = boundRef(bound);
38254
- if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path15}.${side}`));
38255
- }
38256
- if (typeof rule.min === "number" && typeof rule.max === "number" && rule.min > rule.max) {
38257
- findings.push(error51("write.bounds", `${path15}.min`, `is above \`max\` (${rule.min} > ${rule.max}) \u2014 the bound refuses every figure`));
38258
- }
38259
- if (rule.same !== void 0) findings.push(...checkSame(entity, field, rule.same, byAlias, `${path15}.same`));
38260
- if (rule.options_where === void 0) continue;
38261
- if (field.type !== "select_record_link") {
38262
- findings.push(error51("write.options-where", `${path15}.options_where`, `${field.label} is a ${field.type} field \u2014 \`options_where\` narrows the rows a LINK may point at`));
38263
- continue;
38264
- }
38265
- const target = byAlias.get(field.target_entity);
38266
- if (target === void 0) continue;
38267
- const conditions = optionsWhereConditions(rule.options_where, target);
38268
- if (conditions === void 0) {
38269
- findings.push(error51("write.options-where", `${path15}.options_where`, `is not a narrowing this clause can both filter a picker by and refuse a write with: an \`and\` group of plain conditions over ${target.label}'s own fields, each \`field_key\` a field alias and each operator one of ${Object.entries(OPTIONS_WHERE_OPERATORS).map(([type, ops]) => `${type} ${ops.join("/")}`).join("; ")}`));
38270
- continue;
38271
- }
38272
- for (const condition of conditions) {
38273
- const at3 = `${path15}.options_where`;
38274
- const perRow = condition.type === "number" ? contractUnitSource(condition.field, target, entities) : void 0;
38275
- if (perRow !== void 0) {
38276
- findings.push(error51("write.options-where", at3, `compares "${condition.field.label}", which each row reads in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds, with a bare number \u2014 a narrowing compares a figure read in one ${perRow.vocabulary} for every row`));
38277
- }
38278
- if (condition.type !== "select") continue;
38279
- if (condition.field.type !== "select") {
38280
- findings.push(error51("write.options-where", at3, `compares "${condition.field.label}" as a select \u2014 it is a ${condition.field.type} field`));
38281
- continue;
38282
- }
38283
- if (!Array.isArray(condition.value)) {
38284
- findings.push(error51("write.options-where", at3, `narrows "${condition.field.label}" by no option at all \u2014 a select narrowing names the options it admits`));
38285
- continue;
38286
- }
38287
- const declared = new Set(condition.field.options.map((option) => option.alias));
38288
- for (const named of condition.value) {
38289
- if (typeof named === "string" && declared.has(named)) continue;
38290
- findings.push(error51("model.names-declared", at3, `names option "${String(named)}", which "${condition.field.label}" does not declare (${[...declared].join(", ")})`));
38291
- }
38292
- }
38293
- }
38294
- }
38295
- return findings;
38296
- }
38297
- function checkDefaultFrom(entity, field, stated, byAlias, path15) {
38298
- const entities = [...byAlias.values()];
38299
- const [linkAlias, sourceAlias] = stated.split(".");
38300
- const link = entity.fields.find((one) => one.alias === linkAlias);
38301
- if (link === void 0 || link.type !== "select_record_link") {
38302
- return [
38303
- error51("write.default-from", path15, `names "${linkAlias}", which is not a link field of ${entity.label} \u2014 a default is copied from a row this one points at`)
38304
- ];
38305
- }
38306
- if (link.cardinality !== "one") {
38307
- return [
38308
- error51("write.default-from", path15, `reads "${link.label}", which holds MANY rows \u2014 a default copied from a list would take whichever row came back first`)
38309
- ];
38310
- }
38311
- const target = byAlias.get(link.target_entity);
38312
- const source = target?.fields.find((one) => one.alias === sourceAlias);
38313
- if (target === void 0 || source === void 0) {
38314
- return [
38315
- error51("model.names-declared", path15, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
38316
- ];
38317
- }
38318
- const from = resolvedFieldType(source, target, entities);
38319
- const into = resolvedFieldType(field, entity, entities);
38320
- if (from !== into) {
38321
- return [
38322
- error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
38323
- ];
38324
- }
38325
- if (source.type !== "select" || field.type !== "select") return [];
38326
- const findings = [];
38327
- if (source.multi === true && field.multi !== true) {
38328
- findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}", which holds several options, into ${field.label}, which holds one \u2014 a copy would keep whichever came first`));
38329
- }
38330
- const missing = source.options.filter((option) => !field.options.some((one) => one.alias === option.alias));
38331
- if (missing.length > 0) {
38332
- findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" into ${field.label}, which declares no option ${missing.map((option) => `"${option.alias}"`).join(", ")} \u2014 a copied option is written as the one of the same alias`));
38333
- }
38334
- return findings;
38335
- }
38336
-
38337
39561
  // ../shared/src/schemas/workspace_model.ts
38338
39562
  var workspaceModelContractSchema = zod_default.object({
38339
39563
  entities: zod_default.array(contractEntitySchema).describe("The tables this model creates, with their fields, options and views"),
@@ -38901,8 +40125,8 @@ function resultSideEffects(result) {
38901
40125
  }
38902
40126
 
38903
40127
  // src/version.ts
38904
- var VERSION = "0.277.0";
38905
- var APP_SDK_VERSION = "0.106.0";
40128
+ var VERSION = "0.278.0";
40129
+ var APP_SDK_VERSION = "0.106.1";
38906
40130
 
38907
40131
  // src/timezone.ts
38908
40132
  function machineTimezone() {
@@ -38918,7 +40142,7 @@ function machineTimezone() {
38918
40142
  import { spawn as spawn2 } from "node:child_process";
38919
40143
 
38920
40144
  // src/model_reference.md
38921
- var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing a workspace: its tables, fields, options, views, roles,\nfirst rows, how a row of each table is recognised, and the apps over them. Each \xA7\nis its own page at `model/<section>` \u2014 `lotics docs`, or the `docs` tool. Complete models of several trades, to read as\nworked examples: `https://lotics.ai/presets/index.json`.\n\n**What a model composes with**\n\n- **Entities and fields** (\xA7 Entity, \xA7 Field) \u2014 the tables, their columns, options and links.\n- **Records** (\xA7 Records) \u2014 how a row of each entity is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per entity\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, copies, bounds and picks among.\n- **Apps** (\xA7 Apps) \u2014 one register over one entity and the record each row opens: which\n fields go where, the acts, and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no key here changes it.\n A screen the job needs and no key states is a `lotics report` \u2014 the job, what the model\n drew, the word wanted \u2014 never keys bent to approximate it.\n\n**The working order**\n\n1. Name the people and each one\'s JOB \u2014 the work they alone decide or write.\n2. The entities and fields those jobs touch, and a `records` entry for every entity an\n app lists, opens or picks.\n3. One app per job in `apps[]`, each one register over one entity.\n4. `lotics model apply model.json` \u2014 the file is checked first, every problem in one run;\n then the tables, then a new version of every app, live\n (`lotics setup model.json --email you@company.com` where no account exists yet).\n5. Change the file and apply it again; `lotics model pull` writes what the workspace holds.\n Rolling an app back (`lotics run rollback_app`) restores its earlier version \u2014 table\n changes and data writes stay.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not applied \u2014 apply the rest in a second call.\n- **An existing table is adopted.** `lotics model apply` binds an entity whose `label`\n already names a table in the workspace, and adds the fields, options and\n views it is missing. No stored value is ever changed or deleted: a field it\n adopts takes the model\'s `default`, and the `format` and `unit` it reads in,\n where that only relabels \u2014 a date\'s format and a unit converting its figures are\n left. Applying the same model twice changes nothing the second time.\n- **Renaming a field is `lotics run update_table`**, then the same label in this\n file; deleting is `lotics run delete_table`. Neither goes through the file\n (\xA7 What a run remembers).\n- **The file\'s own majority is the language.** A model names no locale \u2014 which\n language it is in is what it mostly says, and `model apply` notes the label\n written the other way. The generated screens read the kit\'s pack, and a\n generated WRITE cannot: its refusals run on the server, so they are worded in\n that same majority. Mix the two and the workspace answers in two languages.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere: sample rows landing among a\n customer\'s real ones cannot be told apart from them.\n- **`lotics model apply` checks all of it before anything is written**, and\n reports every problem in one run rather than the first. Each section of this\n page ends its keys with the rules the check enforces there, one sentence each\n under an id; a finding names its rule\'s id (`[register.filter]`), and\n `model/<rule id>` is that one rule\'s page.\n\n<!-- generated:start rules-model -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `model.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `model.retired` | `field_roles`, `table_workflows` and `connections` are no longer part of a model: `records` states how a row is recognised, and an app\'s own writes do what a table automation did. |\n| `model.names-declared` | Every alias a key names is declared: an entity of this model, a field of the entity the key reads, an option of the select it names, an act of the app, a template or an app of this model. |\n| `model.tables` | A model declares at least one table and at most 50; apply the rest as a second model. |\n| `model.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per model and 2000 documents attached \u2014 a real data set belongs in an import. |\n| `model.language` | *Noted, never refused:* A label or description written in the other language than the model\'s majority \u2014 with or without diacritics. |\n| `model.alias-unique` | An alias is unique where it is named: entities, roles and templates in the model, fields and views in their entity, options in their select. |\n| `model.label-unique` | A label is unique where apply finds it by label: entities, roles and templates in the model, fields and views in their entity, options in their select \u2014 and no view takes its entity\'s own label, which names the whole-table grid apply makes. |\n| `model.template-sha` | A template\'s `content_sha256`, where stated, is the 64-character lowercase hex sha256 of its content. |\n| `model.template-kind` | An act makes its document from an html template; a register\'s `export` fills an html or an excel one. |\n| `model.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n\n<!-- generated:end rules-model -->\n\n## Top level\n\nEvery table of keys on this page is generated from the schema `model apply`\nparses the file with, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### Model file\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entities` | list of [Entity](#entity) | yes | The tables this model creates, with their fields, options and views |\n| `roles` | list of [Role](#role) | no | Workspace groups to create; members are added to them afterwards |\n| `templates` | list of [Template](#template) | no | Document templates: html and email inline, excel made from an uploaded workbook |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records, keyed by entity alias \u2014 written only where every table they land in is empty |\n| `records` | map of alias \u2192 [Record](#record) | no | How a row of each entity is recognised, keyed by entity alias. Every entity an app lists, opens or picks has one |\n| `write_rules` | map of alias \u2192 [Entity write rules](#entity-write-rules) | no | Entity alias \u2192 what a create of that entity finds, copies and refuses |\n| `apps` | list of ([App](#app) \\| [Dashboard app](#dashboard-app)) | no | The apps this workspace will have \u2014 each one register over an entity, or a dashboard of readings |\n\n<!-- generated:end top-level -->\n\n**A model carries no** `fixtures`, `knowledge` or `knowledge_expects`, and no\n`word` / `pdf-form` template: file content is uploaded to the workspace, never\nstated in a model \u2014 an `excel` template names its uploaded workbook by `file_id`. `apps` here is what an agent states to make an app,\nnever built code. An unknown top-level key is an error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because `apply` matches by label.\n\n## Entity\n\n<!-- generated:start entity -->\n\n#### Entity\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable entity alias, unique within the contract |\n| `label` | text | yes | The table\'s name in the workspace, which apply names it |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace |\n| `writes` | `false` | no | false: no person writes this table\'s rows \u2014 only the writes that keep it do, and no app opens or edits one |\n| `fields` | list of [Field](#field) (at least one) | yes | The table\'s columns |\n| `read_scope` | [Read scope](#read-scope) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list of list of alias (at least one) (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link of cardinality "one"). A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n\n#### Read scope\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option)) (at least one) | yes | A row is readable when ANY of these holds |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | alias | yes | A role alias: whoever is in the group it binds to reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A select_member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end entity -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | An entity a create is mounted over \u2014 a register that adds, a rows block \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a declared role (`member_of`) or a field of its entity reached through one-row links: `"self"` a member field, options a single select declares. |\n| `entity.unique` | A `unique` set names two fields or more of its entity once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this entity holding ONE value \u2014 text, number, date, a single select, a link\nof cardinality `"one"` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. `model apply` REFUSES a model where a table\nsome create opens \u2014 a register\'s own, or a record section\'s add \u2014 states none.\n\n**`writes: false` says only the workspace\'s automations write these rows** \u2014 a\nlog of what the system sent, a copy of what another system holds. No app opens,\nedits or files one: a record listing them keeps the section and opens each row\nat rest, with no Add; a screen over the entity operates at most the rows its\nrecord owns; a party of it is picked, never found or minted; and the generator\nwrites no create and no update for it, and never notes it as created nowhere.\nA `lifecycle` on it is refused \u2014 a row walked\nthrough stages is worked by a person \u2014 and so is a publish desk over it. Absent,\npeople write the rows; `true` is not a value.\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved at\napply into the table\'s own row filters, so an app, a workflow reading for a\nviewer, and the API all answer the same rows \u2014 a per-record visibility field the\napp merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every role alias and option alias a clause names\nmust be one this model declares. `apply` writes the rule onto a table it\nCREATES; a table it adopted that ALREADY CARRIES a rule keeps that one, because\nthe rule is the workspace\'s own statement about its rows \u2014 and a run whose model\nstates a different rule reports the entity rather than leaving the claim silent.\n\n**An app may state that its sharing is its read gate: `"reads": "shared"`.** An\napp reads as its owner, so the rule reaches a viewer only as the predicate every\nquery and editor guard of every app over the entity carries \u2014 right for a desk of\none\'s own rows, wrong for a desk whose audience its sharing already decides, where\nwidening the rule meant a role group nobody remembers to fill. Stated on an APP,\nits queries, pickers and guards carry no entity\'s rule and whoever the app is\nshared with reads and writes every row it draws; every other app and the table\'s\nown filters keep the rule. Share it deliberately. Refused on an app none of whose\ntables states a `read_scope`.\n\n**The rows under a private record INHERIT its rule.** An entity that states no\n`read_scope` and hangs under one that does \u2014 through its `parent` link, over one\nhop or several \u2014 is read by the ancestor\'s rule, answered through that link, on\nits table\'s own filters and in every query and guard alike; nothing is restated,\nso a child needs none of the ancestor\'s columns. Stating a rule on the child\nkeeps that one instead, an ancestor with no rule passes nothing down, and a row\nhanging further under the scoped one than a row filter reaches is refused by\nname \u2014 state a rule on it. So is a hop over a link that names more than one row:\nthe rule would admit a reader any one of them admits while the editor\'s guard\nreads the first, so give the link `"cardinality": "one"` or state a rule on the\nchild.\n\n**ONLY the `parent` role is walked.** A register a scoped record reaches by any\nother link \u2014 the rows that NAME it \u2014 is read by that record\'s id with no rule\ntravelling to it, so it is refused until it states one of its own.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (a default is not applied to them), and a required\nLINK is written with its row: the entity it names is created first, and entities\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"select_member"` \\| `"select_record_link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `alias` | alias | yes | Stable local alias, unique within the entity |\n| `label` | text | yes | The field\'s name in the workspace, which apply names it |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Apply writes it onto the field, and every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `format` | `"number"` \\| `"currency"` \\| `"percentage"` | no | What the figure is \u2014 a plain number, money in `currency`, or a percent |\n| `currency` | text | no | ISO 4217 code |\n| `unit` | text | no | What a plain figure counts or measures, drawn after it: a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does. Only beside format "number". |\n| `unit_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number". |\n| `currency_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency". |\n\n<!-- generated:end field-number -->\n\n`format` is what the number IS, and every surface reads it: `currency` prints as\nmoney in the code the row or the field states, `percentage` as a whole percent\nwith its sign. An ABSENT number is drawn absent \u2014 the one exception is a\n`sum` or count rollup the plan reads as a **`measure`**: that is the thing\naccumulated toward a bound, so nothing accumulated yet is zero and the meter\ndraws it. The same rollup read as an `amount` keeps its blank, and so does every\nother role: nothing added to what a row is WORTH means unpriced, not free. A\nformula reading only such sums and counts, and reading zero where each does\n(`{received} - {refunded}`), is one too. A `min`, an `avg`, a percentage of\nnothing and every other formula stay blank in any role, because none of them has\nan answer to give. This is why the pair on one\nscreen reads two ways \u2014 what has come in against what is owed \u2014 and why a\nmeasure\'s own LIMIT, an amount, leaves an unquoted row out of the count rather\nthan reporting it as nothing collected. **AND WHERE THAT LIMIT IS ABSENT \u2014 OR\nZERO \u2014 THERE IS NO LEVEL AT ALL**: a level is a reading AGAINST a bound, so a row\nthat states no bound, or a bound of nothing, draws nothing \u2014 cell, fact and all \u2014\nrather than a numerator whose whole meaning was the comparison. "Collected 0"\nbeside a blank total reads as money against a job worth nothing, and "0 of 0"\nagainst a count of nothing owed claims a comparison nobody can make. A measure the model gives no limit is a plain figure\nand is unaffected. A share is stored in percent units \u2014 68.1 is 68.1 % \u2014 and the\ncolumn, the fact behind it, the meter it is judged by and the figure over the\nregister all say so.\n\n`unit` is what a plain figure counts or measures, and stands only beside\n`format: "number"`. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own in `formula.unit`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`lotics run update_table`. `model apply` never makes it: the model states the\nworkspace\'s unit until then. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow in `unit_field` or `currency_field` (a formula in `formula.unit_field` /\n`formula.currency_field`) \u2014 or a lookup of one through a one-link, a line\nreading its shipment\'s currency. That select\'s options are the vocabulary:\neach label a unit as `unit` takes one (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit, a\nchart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this entity \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused, as is any lookup of such a figure (its\nunit lives on the other row). A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | yes | The colour the option\'s badge is drawn in |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) or a kit glyph ({kind: "icon", name: "wrench"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another entity. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), or a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `apply` writes the\nmarks onto the table, sets one an adopted option lacks, and reports one it wears\ndifferently rather than overwrite it. The glyphs:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, arrow-down, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, ban, banknote, bed, bell, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, clipboard-list, clock, code, code-xml, columns-3, columns-3-cog, construction, container, copy, credit-card, database, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grip-vertical, group, hand-coins, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, scan, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sliders-horizontal, smile, smile-plus, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, table, table-2, tag, target, text-quote, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap\n\n<!-- generated:end field-select-glyphs -->\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-select_member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `multi` | boolean | no | Allow multiple selections |\n\n<!-- generated:end field-select_member -->\n\n### `select_record_link`\n\n<!-- generated:start field-select_record_link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `target_entity` | alias | yes | Alias of the entity this field links to |\n| `sync_both_ways` | boolean | no | Create a paired link field on the target entity for bidirectional sync |\n| `paired_field_alias` | alias | no | The pair edge of a bidirectional link: the field alias ON THE TARGET ENTITY that is this link\'s sync partner. Both sides of a pair carry it, each naming the other. Apply creates whichever side it reaches first WITH the pairing (the platform auto-creates the partner) and binds the partner alias to the auto-created field \u2014 without this edge the two contract fields would be created independently and collide with the auto-created partner. |\n| `cardinality` | `"one"` \\| `"many"` | no | How many linked records this field holds. Default \'many\'. \'one\' holds a single row and needs no partner; where the link IS paired, the partner side holds many. |\n| `display_field_aliases` | list of alias | no | Field aliases on the target entity shown as the link\'s display text / picker columns |\n\n<!-- generated:end field-select_record_link -->\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused.\n\n**A single-valued link needs no partner.** `"cardinality": "one"` on its own is a\nlink that holds one row \u2014 one customer on an invoice, one project on a device \u2014\nand nothing is created on the target. The mirror invariant belongs to a PAIRED\nlink: pair a link when the target\'s own record should list what points at it, and\nleave it unpaired when it should not. Either way the record plan draws the\nrelation as a section on the side it points at, so an unpaired link costs the\ntarget nothing.\n\n### `files`\n\nNo keys beyond every field\'s; a row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n### `formula`\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | [Formula](#formula) | yes | Formula config. The expression references other fields on the SAME entity by alias in braces, e.g. `{quantity} * {unit_price}`. |\n\n#### Formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `format` | `"number"` \\| `"currency"` \\| `"percentage"` \\| `"link"` | no | Display format. \'number\' / \'currency\' / \'percentage\' for numeric results; \'link\' for text-output formulas that return a URL \u2014 renders the result as a clickable link. |\n| `currency` | text | no | ISO 4217 currency code, e.g. USD, VND, EUR |\n| `unit` | text | no | What a plain figure counts or measures, drawn after it: a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does. Only beside format "number". |\n| `unit_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number". |\n| `currency_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency". |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | What the expression YIELDS \u2014 the kind the platform infers at write time, declared here so the offline checks can read it. `format` beside it is how that result is drawn, not what it is. Ignored on the wire (the platform re-infers it); `lotics model pull` writes the inferred value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring neither it nor a `format` says nothing about its\nresult, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a model has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the copy writes that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\nentity is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The offline check and the platform compute a formula with one\nengine, and the check refuses a call or a name it cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n)\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to roll up from |\n| `aggregate_option` | [aggregation](#rollup) | yes | Aggregation operation. Its `field_key` names a field alias on the linked entity. |\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n\n<!-- generated:end field-rollup -->\n\n`aggregate_option` is `{ "operation": \u2026, "field_key": \u2026 }` \u2014 `field_key` a field\nalias on the linked entity (`count` may omit it), and `operation` one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to look up through |\n| `lookup_field_alias` | alias | yes | Alias of the field on the linked entity to display |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_key` | text | yes | A field alias on the linked entity the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n\n<!-- generated:end field-autonumber -->\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A `unit_field` or `currency_field` is a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a declared entity: its `display_field_alias` a field of it, its `paired_field_alias` a link on it naming this one back \u2014 and of two paired links at most one reads one row. |\n| `field.formula` | A formula parses, reads fields of its own entity by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.rollup` | A rollup aggregates, through a link of its entity, a field of the linked entity by an operation a model declares and that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked entity. |\n| `field.lookup` | A lookup reads, through a link of its entity, a field of the linked entity \u2014 never one read in each row\'s own unit or currency \u2014 ordered by a field of it. |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its entity. |\n\n<!-- generated:end rules-view -->\n\n## Roles\n\nA role becomes a workspace group.\n\n<!-- generated:start roles -->\n\n#### Role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable role alias, bound to a workspace group when the model is applied |\n| `label` | text | yes | The group\'s name in the workspace |\n\n<!-- generated:end roles -->\n\n## Templates\n\nAn `html` or `email` template carries its content inline, and `{{name}}` in it is filled\nfrom the workflow\'s data; an `excel` template names an uploaded workbook by its `file_id`,\nfilled with the register\'s report by its `export` alone.\n\n<!-- generated:start templates -->\n\n#### Template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"html"` \\| `"email"` \\| `"excel"` | yes | html is a page a workflow renders to a PDF; email is a message a workflow sends; excel is a workbook a register\'s export fills |\n| `alias` | alias | yes | Stable template alias, unique within the contract |\n| `label` | text | yes | The template\'s name in the workspace |\n\n<!-- generated:end templates -->\n\n### `html` and `email`\n\n<!-- generated:start template-inline -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `content` | text | yes | Inline template content; placeholders reference field aliases |\n| `content_sha256` | text | no | sha256 (64-char lowercase hex) of the utf-8 content; derived where the template is written when absent |\n\n<!-- generated:end template-inline -->\n\n### `excel`\n\n<!-- generated:start template-excel -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `file_id` | text | yes | The uploaded .xlsx (`fil_\u2026`) the template is made from; its markers read what the export fills it with |\n\n<!-- generated:end template-excel -->\n\nA paper that has to look like a counterparty produced it \u2014 an official letter,\nan acceptance minute, a supplier\'s bill \u2014 is the same `html` template with a\nshell around the body: a letterhead, a reference line, a seal and a signature\nblock, and paper grain over everything. One shell, many bodies; the data is the\nonly thing that changes, so a workflow can re-issue it over any record.\n\n```jsonc\n{ "alias": "cong_van", "label": "C\xF4ng v\u0103n", "type": "html",\n "content": "\u2026the page below, as one JSON string\u2026" }\n```\n\n```html\n<style>\n .sheet{position:relative;width:718px;padding:44px 58px 30px;background:#fbfaf6;color:#111;font:14.2px/1.5 \'Liberation Serif\',serif}\n .grain{position:absolute;inset:0;opacity:.34;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'140\' height=\'140\'><filter id=\'f\'><feTurbulence baseFrequency=\'.9\' numOctaves=\'2\'/><feColorMatrix values=\'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .35 0\'/></filter><rect width=\'140\' height=\'140\' filter=\'url(%23f)\'/></svg>")}\n .top{display:flex;text-align:center;font-size:13.4px} .top>div{flex:1} .u{display:inline-block;border-bottom:1px solid #111;font-weight:700}\n .ref{display:flex;text-align:center;font-size:13.4px;margin-top:6px} .ref>div{flex:1} .ref .r{font-style:italic}\n h1{text-align:center;font-size:15.6px;margin:26px 0 18px} p{text-align:justify;text-indent:26px;margin:0 0 9px}\n .sig{display:flex;margin-top:20px} .sig .l{flex:1} .sig .r{width:290px;text-align:center;position:relative}\n .sig .nm{font-weight:700;margin-top:96px} .seal{position:absolute;left:4px;top:8px;width:166px;height:166px;opacity:.66;mix-blend-mode:multiply;transform:rotate(-17deg)}\n </style>\n <div class=\'sheet\'><div class=\'grain\'></div>\n <div class=\'top\'><div><b>{{issuer_parent}}</b><br><span class=\'u\'>{{issuer}}</span></div>\n <div><b>C\u1ED8NG H\xD2A X\xC3 H\u1ED8I CH\u1EE6 NGH\u0128A VI\u1EC6T NAM</b><br><span class=\'u\'>\u0110\u1ED9c l\u1EADp - T\u1EF1 do - H\u1EA1nh ph\xFAc</span></div></div>\n <div class=\'ref\'><div>S\u1ED1: {{number}}</div><div class=\'r\'>{{place}}, ng\xE0y {{day}} th\xE1ng {{month}} n\u0103m {{year}}</div></div>\n <h1>{{title}}</h1>\n <p>K\xEDnh g\u1EEDi: {{recipient}}.</p>\n {{{body}}}\n <div class=\'sig\'><div class=\'l\'><b>N\u01A1i nh\u1EADn:</b><br>- Nh\u01B0 tr\xEAn;<br>- L\u01B0u VT.</div>\n <div class=\'r\'><img class=\'seal\' src=\'{{seal_url}}\'><b>{{signer_title}}</b><div class=\'nm\'>{{signer}}</div></div></div>\n </div>\n```\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) \u2014 never a files field or a computed select. On one record it is\nhanded, too, the rows of each child the record draws as `rows` whose alias the\ntemplate names, listed under that alias for `{{#each <alias>}}` in place of the\nrecord\'s own link to them: every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the entity\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model, attaching at most 2000 documents between them \u2014 a real data set\nbelongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its entity. |\n| `rows.required` | A row states every field its entity requires: it is created with what it states, its fields\' defaults unapplied. |\n| `rows.distinct` | Open rows of an entity a register lists read apart: no two state the same title and the same line under it. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<entity>:<ref>"` of a row of the linked entity in this file for a link, `"self"` for a member, a relative path beside the file or a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\nA `files` cell attaches documents: paths relative to this file (no `..`, never\nabsolute), which the check proves exist and `apply` uploads into the workspace\nbefore any row is written \u2014 a paperwork business seeds its papers with its\nrows. The server accepts only `fil_` ids of files this workspace owns, which is\nwhat the upload leaves behind. After a run that wrote rows, the WORKSPACE holds\nwhich record each row became, under the row\'s own `<entity>:<ref>`:\n`delete_records` over them is how a seeded set is reset, and applying again\nre-dates it.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | paths beside this file \u2014 `["scans/pccc_letter.png"]` \u2014 uploaded by `model apply`/`setup` before the model is sent; or `fil_` ids of files already in this workspace |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of each entity is RECOGNISED \u2014 once per entity, keyed by\nentity alias \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every entity an app lists, opens or picks has one.\n\n```jsonc\n"records": {\n "visit": {\n "title": "container_no",\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n },\n // A release order read against what it allows: a meter wherever it shows.\n "release": { "title": "release_no", "figure": "issued", "limits": { "issued": "units" } },\n // A dossier\'s stage is the last date it reached \u2014 no stored select.\n "dossier": { "title": "applicant", "status": { "milestones": ["received_on", { "field": "appraised_on", "when": { "kind": ["loan"] } }, "signed_on"], "closed": ["signed_on"] } }\n}\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | alias | yes | The field that names a row \u2014 the first thing every surface shows: who or what it is, a link naming it by the linked row (a job read by its customer). An autonumber is refused; a typed number is the subtitle |\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party) |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through \u2014 a single select or its milestone dates \u2014 drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | no | The single select that is the row\'s state \u2014 or `milestones` instead |\n| `milestones` | list of (alias \\| [Milestone](#milestone)) (at least 2) | no | Dates in the order the work reaches them, instead of a stored select: the stage is the last one filled, ticking one stamps today, and clearing one clears every later one |\n| `closed` | list of alias (at least one) | no | Options (or milestones) that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `history` | alias | no | A child entity every status change appends one row to (its link to this entity, the new status, the moment and who) \u2014 written by the app\'s own writes, never a table automation; a row the model seeds opens it at its status, the day the rows land, unless the model states its rows; with `field` only |\n\n#### Milestone\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A date field: the stage is reached on the day it holds |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | yes | The stage applies only while each named select holds one of these options, and each named stored yes/no is this value; otherwise the row skips it |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title names the row by what it is \u2014 never an autonumber; a code is its subtitle. |\n| `records.image` | `image` is a files field, never one an act keeps the documents it makes in (`into`). |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a section\'s too. |\n| `records.status` | A status is one single select, `closed` naming options of it \u2014 or its `milestones`. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.history-derived` | A `history` the model does not declare is derived and keeps one entity\'s moves, labelled by that entity ("L\u1ECBch s\u1EED <entity label>", or "<entity label> history"): no declared table holds that label, and the entity has no other field under the link it adds. |\n| `records.history-scope` | A status history reads by its record\'s `read_scope`, answered through its one-row link to the record within the links a row rule reaches \u2014 or it states its own. |\n| `records.milestones` | Milestones are dates a person ticks, each named once; `closed` names some of them, and a deadline (`due`) is never one. |\n| `records.milestone-add` | An add asks the first milestone at most \u2014 each after it is ticked in order. |\n| `records.figure` | A figure is a number. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and an add that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). An autonumber is refused as a title; a typed number is the\n subtitle.\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` entity gets one row per move \u2014 its link to this\n entity, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the app\'s own writes that move the status, never by a\n table automation. A `history` the model does not declare is derived, and needs no\n `records` entry of its own.\n- **Milestones** are the alternative to a stored select: dates in the order the work\n reaches them, the stage being the last one filled (never stored). A milestone with a\n `when` applies only while its select holds those options and its stored yes/no that\n value; otherwise the row skips it.\n The record draws them as one block where ticking stamps today and clearing one clears\n every later one; every save and act re-checks that a date comes after each earlier one\n that applies. `closed` names the milestones that end the work. A register\'s add may ask\n the first milestone, never a later one; an act may stamp one, never clear it.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the entity says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Write rules\n\n`write_rules` is what a WRITE meets, keyed by entity alias; each entity\'s is held\nby its table. Every generated write of an\napp (`create_<entity>`, `update_<entity>`, each act) re-checks these on the\nserver; the screen only mirrors them.\n\n```jsonc\n"write_rules": {\n // A customer is RECOGNISED by their address. An add that names one \u2014 from the\n // customers register or from a picker\'s "new" \u2014 reuses the row it matches and\n // opens one only where nothing does, so the book never grows a second Acme.\n "customer": { "natural_key": ["email"] },\n "order_line": {\n "fields": {\n // A line of nothing is not a line. Refused on create and on update.\n "quantity": { "min": 1, "max": 9999 },\n // Shipped inside its order\'s window: a bound read off the row a link names\n // is held from both sides \u2014 moving the order\'s dates never strands a line.\n "ships_on": { "min": "order.placed_on", "max": "order.due_on" },\n // The price is fixed at the moment of ordering \u2014 COPIED off the product,\n // not looked up for ever after.\n "unit_price": { "default_from": "product.price" },\n // Nothing is sold off an empty shelf. The picker reads only the rows that\n // answer this, and the write refuses the same rows again.\n "product": {\n "options_where": {\n "node_type": "group", "logic": "and",\n "children": [{ "node_type": "condition", "type": "number",\n "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n },\n // One booking per room at a time, while it is held: a create or a save whose\n // span overlaps another held booking of the same room is refused.\n "booking": {\n "fields": { "ends_on": { "min": "starts_on" } },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n}\n```\n\n<!-- generated:start write-rules -->\n\n#### Entity write rules\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches. |\n| `fields` | map of alias \u2192 [Field write rule](#field-write-rule) | no | Field alias \u2192 what that field\'s value is copied from, bounded by or picked among |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused |\n\n#### Field write rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default_from` | text | no | Copy this field\'s value from the linked row at CREATE time \u2014 `<link alias>.<field alias>`, the link being a one-row link on this entity, required or filled by the add (the record a row is added under). The value is copied rather than looked up, so the source changing later leaves the row alone. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (`unit_field`, `currency_field`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity\'s own fields, each `field_key` a field alias. It narrows the picker\'s read and is refused again where the write lands. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both entities to the same entity. It narrows the picker\'s read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The stored date (or datetime) a row\'s span starts on |\n| `to` | alias | yes | The stored date (or datetime) a row\'s span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of alias \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed); absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the entity\'s `unique`. |\n| `write.default-from` | `default_from` copies `<link>.<field>` off the row a one-row link names, wherever an add fills that link (asked, or the record it is added under), into a field of the same type: a select into one declaring each of its options by alias, several options only into a multi-select. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (`unit_field`, `currency_field`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.same` | `same` stands on a link and names one-row links this entity and the link\'s target both hold to one entity. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields; `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the entity\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `min` or `max` naming a field is read off the row as the save would leave it,\n and one naming `<link>.<field>` off the row the link names \u2014 the link\'s side\n re-checks its own rows when that bound moves.\n- A `default_from` copies where the add is sure to hold its link\'s row \u2014 a\n required link, or the record the row is added under (an appointment added under a\n plan takes the plan\'s patient) \u2014 and the two field types match. The copied field is\n not asked there; through an optional link the add leaves empty, it is asked.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both entities hold to one\n entity; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n moving what the two compare, on either row, is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused again\n where the write lands unless it holds the narrowing. A link naming one kind of\n a book\'s rows (a "Shipping line" among the parties) narrows to that kind, or\n its picker offers every row.\n\nA field\'s own `required` is not here: the contract carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## Apps\n\nAn app is ONE register over one entity and the record each row opens \u2014 or a dashboard\nof readings. The author states COMPOSITION \u2014 which fields go where; the runtime owns\nhow each thing looks, one treatment per concept. Nothing is drawn that the app does not\nname, and each fact appears once on a record. What the vocabulary has no word for is an\nact\'s own `workflow`.\n\n```jsonc\n"apps": [{\n "alias": "gate", "name": "Gate in and out", "entity": "visit",\n "register": { "columns": ["service"], "filters": ["customer", "service"] },\n "record": {\n "sections": [\n { "title": "In", "fields": ["customer", "service"], "blocks": [{ "rows": "fee", "columns": ["amount"] }] },\n { "title": "Out", "at": ["in_yard"], "fields": ["release", "seal"], "blocks": [{ "files": ["photos"] }], "acts": ["gate_out"] }\n ]\n },\n "acts": [{ "alias": "gate_out", "label": "Gate out", "when": { "stage": ["in_yard"] },\n "requires": ["release", "seal"], "set": { "stage": "gone", "left_on": "now" }, "confirm": true }],\n "checks": [{ "field": "unpaid", "blocks": ["gate_out"] }]\n}]\n```\n\n<!-- generated:start apps -->\n\n#### App\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `entity` | alias | yes | The entity whose rows this job works \u2014 the register\'s rows |\n| `books` | list of [Book](#book) (at least one) | no | Other entities the register reads as one list with `entity`\'s rows, each opened, edited and acted on in its own table: the same app over each, its fields lined up with `entity`\'s. Only a table or cards |\n| `scope` | alias | no | A required one-row link of the entity: the app works inside one row of the linked entity at a time (a project, a branch), picked from a list of them, and every row it reads, counts and adds is that row\'s |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `writes` | `"children"` | no | The reader adds and edits the record\'s child rows but not the record\'s own fields; the app\'s acts still move it |\n| `row` | [App row](#app-row) | no | How this job reads the entity\'s rows where it reads them otherwise than `records` (a cashier\'s figure is what is owed): the subtitle, figure and image stated replace the entity\'s wherever this app draws its rows \u2014 the register, a card, the record\'s header, a calendar, lane or roster entry. The title stays the entity\'s |\n| `register` | [Register](#register) | no | The rows: which columns and filters, the order, how a row is added |\n| `record` | [Record page](#record-page) | no | What a row opens: the door, its sections, and beside them its comment thread and its status history where stated |\n| `acts` | list of [Act](#act) | no | What the reader does to a record \u2014 each a press with its conditions and its write |\n| `checks` | list of [Check](#check) | no | Formulas that warn while they stand, or refuse the acts and saves they name |\n\n#### Theme\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `color` | text | yes | The tile\'s colour |\n\n#### App row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title in this app, in place of the entity\'s |\n| `figure` | alias | no | The one number a row stands for in this app, in place of the entity\'s; a `limits` bound on it reads as its meter |\n| `image` | alias | no | A files field that pictures a row in this app, in place of the entity\'s |\n\n#### Book\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another entity whose rows the register reads beside its own \u2014 the same job over another table (a legacy ledger, a second branch\'s book) |\n| `fields` | map of alias \u2192 alias | no | A field of the app\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ; a field the app reads lines up by its own alias otherwise, and one this entity lacks is blank on its rows and drops from what they open \u2014 an act, check or add needing it is not offered on them. Options line up by alias |\n\n#### Dashboard app\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `dashboard` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [List](#list)) (at least one) | yes | The readings, in order \u2014 each over every row of its entity, windowed by the one period the reader switches |\n\n<!-- generated:end apps -->\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.alias-unique` | An app\'s alias is unique among the model\'s apps, and an act\'s among its app\'s acts. |\n| `app.records-entry` | Every entity an app lists, opens or picks \u2014 its own, a block\'s, a linked row\'s, a document\'s \u2014 has a `records` entry. |\n| `app.reads-shared` | `reads: "shared"` stands only where a table the app reads states a `read_scope`. |\n| `app.writes-children` | An app that writes only the record\'s children (`writes: "children"`) adds no row of its own entity. |\n| `app.scope` | `scope` is a required one-row link of the app\'s entity, the same on every row the app lists \u2014 never a column, filter, search field, group, `where`, `opens`, section field or add question. |\n| `app.when` | A `when` holds single selects to options and yes/nos to `true` or `false`, stored or a formula of one \u2014 a milestone\'s `when` only a stored yes/no, read before any formula over it is computed. |\n| `app.asked-written` | What an add, an act or `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a section\'s field, an add\'s question or an act\'s ask: those rows are the record\'s own, read as a `rows` block. |\n| `app.create-asks` | An add asks fields of a table people write \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | An add (`create`) asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register app\'s `books` each name another entity once \u2014 never the app\'s own \u2014 whose fields line up with the app\'s by alias, else by `fields` (a field the app reads, or one a report prints \u2192 one of the book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each book holds the title, the status, the folder (`scope`), what `where` keeps and every check locking the rows; the register is a table or cards. Two books holding a field of their own under one alias hold it as one kind of value. A report\'s `books` each name the app\'s entity or one of its books, once, and one of them holds each field its `where` and `per` narrow by. |\n| `app.book-lacks` | *Noted, never refused:* A book lacking a field the app\'s record, an act or an add reads: what reads it drops from the book\'s rows \u2014 an act, a block, a section, the add. A field only a report prints, held by a book as another kind of value, prints blank on its rows. |\n| `app.row` | An app\'s `row` states only what differs from `records`: a line that is not the title, a number for its figure, a files picture no act makes documents into. |\n| `app.files-shown` | Every files field is drawn by some app over its entity \u2014 a section\'s `fields`, a `files` block, a column, the `image`, or where an act making papers into it stands: the field an act keeps its document in (`into`) is placed where the paper reads. |\n| `app.document-image` | *Noted, never refused:* A linked entity holding one files field and no `image`: `image` lets a link preview its paper. |\n| `app.tasks` | *Noted, never refused:* A task entity an app lists whose rows no write of the app moves to a closed option (its ring stands disabled), or that an act ticks done and none moves back. |\n\n<!-- generated:end rules-app -->\n\n`writes: "children"` is a desk that adds and edits a record\'s child rows and never\nthe record itself. `reads: "shared"` lifts every read scope the app\'s tables state \u2014\nrefused where none of them states one.\n\n`scope` names a required one-row link of the entity: the app works inside ONE row of\nthe linked entity at a time \u2014 a project, a branch, a season. With no folder remembered\nit opens on the list of them, each with how many of the app\'s rows it holds; inside,\nevery read, count and reading narrows to that one, an add files its row under it (the\nlink is never asked), and the record states its folder in the header. A scope narrows\nthe view; who may read what stays the entity\'s `read_scope`. The folder link is never a\ncolumn, a filter, a search field, a fact or an add\'s question.\n\nA dashboard is `{ "alias", "name", "dashboard": [readings] }` with no entity, register\nor record: each reading reads every row of its entity, and one period the reader\nswitches (this month \xB7 30 days \xB7 this quarter \xB7 this year) windows each reading placed\nin time; one placed in no time reads the rows as they stand now, and its head says so.\nA `list` reading lists rows, each opening in its `app`. The readings answer the one\nquestion the dashboard\'s owner asks, in the order they ask it \u2014 rows to act on lead where\nthe answer is rows; no order of kinds is the rule.\n\n```jsonc\n// The depot owner\'s morning question: which boxes are past their free days, whose are they, is it growing?\n{ "alias": "overview", "name": "Depot overview", "dashboard": [\n { "list": "visit", "where": { "overdue": true }, "columns": ["line"], "app": "gate", "label": "Past free days" },\n { "breakdown": "visit", "by": "line", "where": { "overdue": true }, "label": "Past free days by line" },\n { "trend": "visit", "over": "arrived_on", "where": { "overdue": true }, "label": "Past free days, by arrival" }\n] }\n```\n\n### Register\n\n```jsonc\n"register": { "columns": ["service", "release"], "filters": ["customer", "service"], "create": ["container_no", "customer"] }\n```\n\n<!-- generated:start apps-register -->\n\n#### Register\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `columns` | list of alias | no | Fields read as columns after the row\'s title \u2014 on a calendar\'s entry or a lane\'s block, its facts after its title; never what the row draws itself (its subtitle, image, status, figure, or the bound its figure\'s meter reads against), which the runtime places |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The fields the reader narrows the rows by every day, in that order \u2014 each earns its place by that daily use, so none to 3 is normal and 3 is a cap, never a quota (a select, a member, a link, a date, a yes/no, a number as a range, or its `tiers` where the business narrows by those bands daily) \u2014 each on a desk\'s line, in one Filters sheet on a phone. Never the status (its chips); each a field the rows show \u2014 a column, or a part of the row. A threshold the business names ("large orders") is a formula yes/no here; a column\'s header sorts by a day or an amount |\n| `search` | list of alias (at least one) | no | The fields the search box matches (a link by its row\'s title); a pasted list searches each line and names the lines no row matched. Absent, every word of the row |\n| `group` | alias | no | The field the rows open grouped by, the reader\'s grouping starting there \u2014 a period\'s rows read under each period, one per person; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order rows open in: one key, or a list of keys most significant first, each breaking the ties of the ones before it (a select orders by its options\' order); absent, the first deadline soonest first, else the newest first |\n| `tabs` | list of [Register tab](#register-tab) (2\u20135) | no | The lists the register reads its rows as, each a tab with its count over one period the reader picks above them \u2014 the stock at its end beside the arrivals and departures during it, a report\'s sheets; each keeps the register\'s filters and search. Absent, one list. Not with a calendar, roster or lanes, which read their own window |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | With `tabs`: the period they open on \u2014 `today`, `week`, `month`, `quarter` or `year` so far, or `{ days }`, the last that many days through now (a night shift running past midnight reads two); absent, this month so far |\n| `layout` | `"table"` \\| `"cards"` \\| `"calendar"` \\| `"roster"` \\| `"lanes"` | no | `cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); absent, a table |\n| `roster` | alias | no | With `layout: "roster"`: the entity whose rows fill the days \u2014 one single link to this entity, and a date on its row line (`records` title or subtitle). A cell reads its row\'s status, else the first single select on its line, several rows a mark each, the cell read by the one latest in that select\'s options; a row whose line holds a second date after the first fills every day to it; a day with none stays empty, never an absence |\n| `expect` | alias | no | With `roster`: a single select of the roster\'s rows whose options are the columns in place of days \u2014 each cell the row holding that option, read by its status, an empty one added there (a checklist of papers per case); the roster then has no period |\n| `of` | alias | no | With `expect`: this entity\'s own multi-select holding the options each row expects \u2014 its other columns stand blank on that row, never added. Its options are among `expect`\'s |\n| `lanes` | alias | no | With `layout: "lanes"`: a one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty. A row is a block from the first date on its line to the second \u2014 within one day by the hour where the first holds one, else by the day over one day, a week or a month; a free stretch between blocks adds a row there, its lane, its start and the end the stretch reaches filled |\n| `loads` | map of alias \u2192 alias | no | With `lanes`: what a lane carries against what it holds, at most 2 \u2014 a number of this entity each block adds (a weight) to a number of the lane\'s row it is held within (a payload), in one unit or two of one dimension. Each lane reads the most its blocks add up to at one time in the window \u2014 a day\'s rows together \u2014 and names by how much and when a lane runs over |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose select holds one of these options and whose yes/no (stored, or a formula) is this value \u2014 read so on the server and never offered as a chip: the rows this app works of an entity other apps read whole (the purchases, of orders both ways). A row added here holds a select\'s one option and a stored yes/no\'s value |\n| `opens` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The view the register opens on: each entry a narrowing on a field the rows show that stands as a chip the reader may take away (the rows still owed), `"me"` a member field holding the reader (the rows mine). One on the status replaces the chips\' opening on the open work |\n| `create` | list of alias (at least one) \\| `false` | no | The fields asked when a row is added; absent, the title, the subtitle and figure no default fills, the natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `export` | `true` \\| list of [Register report](#register-report) (at least one) | no | The rows in view saved as the register\'s report \u2014 its title, a line per thing narrowing the rows, the readings and every row: `true`, one workbook of the register\'s columns; a list, the reports a reader picks from one export menu |\n| `readings` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot)) (at least one) | no | Readings of the register\'s own entity above its rows, over the rows in view \u2014 the search, the status and the filters narrow them, but a picture\'s own field, read as the register opens on it. A press on a breakdown\'s part, a pivot\'s cell or a figure with `where` narrows the rows by its field, as a filter does |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded by the field\'s own format, several picked at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n#### Register tab\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tab` | alias | yes | The tab\'s name: its address, and the key its rows stand under in a report |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `in` | alias | no | A date (stored, or a formula): the tab holds the rows dated within the register\'s period, from its start through its last day, or up to its last minute \u2014 the arrivals of a period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun before it, and not ended by then \u2014 what is in stock, out on hire or still owed at that moment |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in, as the register\'s `sort`; absent, the register\'s |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on (stored, or a formula) |\n| `to` | alias | yes | The date a row ends on (stored, or a formula), empty while it is open |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n#### Register report\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The report\'s name, in the reader\'s words \u2014 its entry in the export menu and its file\'s title |\n| `description` | text | no | What the report holds, under its name in the menu |\n| `template` | alias | no | A template filled with the report \u2014 `title`, `lines`, `readings`, `at` (when it was made), `dates` (each date filter\'s `from` and `to`, by field), `period` (the tabs\' `from` and `to`), `per` (the value picked), every row in view (at most 20,000) under the entity\'s alias, and each tab\'s under its name: an html one made a PDF, an excel one a workbook; absent, the workbook of the register\'s columns, a sheet per tab |\n| `filename` | text | no | The file\'s name, in the template grammar over one value each \u2014 `title`, `at`, `dates.<field>.from` or `.to` of a date filter, `period.from` or `.to` with tabs, `per` with a `per`, `lines \\| lookup:<n>` \u2014 `Stock {{per}} {{period.to \\| format:"dd.MM.yyyy"}}`; absent, the template\'s name, or the report\'s title |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The rows the report is of, set on the register as the chips they are before its file is made \u2014 the screen shows what the file holds |\n| `per` | alias | no | A select, a member or a one-link the reader picks one value of from the menu \u2014 the rows narrowed to it, as its chip, and one file of that value |\n| `books` | list of alias (at least one) | no | The rows of these books only \u2014 each the app\'s `entity` or one of its `books`; absent, every book\'s |\n\n<!-- generated:end apps-register -->\n\n<!-- generated:start rules-register -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.export-report` | A register\'s reports each have their own label; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `register.export-template` | A template an export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the register entity\'s alias \u2014 an entity named none of those \u2014 and is the document of no act. |\n| `register.column-drawn` | A column never names what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and names a field once. |\n| `register.filter` | A filter names a select, a member, a link, a date, a yes/no or a number of the register\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `register.filter-split` | *Noted, never refused:* A filter on a field a breakdown or pivot in the band splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `register.filter-unshown` | A filter, a narrowing the register `opens` on, and a press in its band (a breakdown\'s part, a pivot\'s cell, a figure\'s `where`) name a field its rows show \u2014 a column; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under. A narrowing to the reader (`"me"`) needs none. |\n| `register.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `register.search` | `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `register.sort` | Each `sort` key names a field of the register\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `register.tabs` | A register\'s `tabs` each name a tab of their own, never a key a report already holds; a tab\'s `in` and `open` dates are dates of its rows, `open`\'s two different; its `where` and `sort` hold as the register\'s do, and never narrow by the status, which its chips split. Only a table or cards reads tabs, and a band reading\'s `tab` names one of them. |\n| `register.group` | `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and grouping by the title needs a subtitle to lead each row. |\n| `register.readings-own` | A register\'s `readings` read its own entity; another entity\'s stand on a dashboard. |\n| `register.readings-restate` | A register\'s metric never sums the row\'s figure or counts the rows without a `where`: the summary line totals the figure and the status chips count the rows. |\n| `register.where-opens` | `where` and `opens` hold the register\'s own selects and yes/nos, and `opens` a member field holding the reader (`"me"`); a field `where` fixes is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. |\n| `register.roster` | `layout: "roster"` and `roster` go together: the roster\'s entity has one single link to this one and a date on its line, a roster draws no `columns`, `expect` needs `roster` and `of` needs `expect`. |\n| `register.roster-expect` | A roster\'s `expect` is a single select of its rows, and `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n| `register.lanes` | `layout: "lanes"` and `lanes` go together: `lanes` is a one-row link to an entity with a `records` entry, the rows name a date on their line, a block\'s two ends are read alike (two days or two moments), and `loads` needs `lanes`. |\n| `register.loads` | `loads` names one to 2 numbers a block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n| `register.band-pivot` | *Noted, never refused:* A pivot in a register\'s band: its grid reads on a dashboard, and the band leads with a `metric`. |\n| `register.band-headline` | *Noted, never refused:* A register over rows that move (a status or a deadline) stating no `readings`: the band leads with the job\'s headline number. |\n\n<!-- generated:end rules-register -->\n\nThe row is the entity\'s `records` entry: its picture, title and the line under it,\nthen the status, the columns, and the figure at the right; the title, the status and a\ncolumn of days or amounts sort the rows by their header. The status is always the chips, with\ncounts, opening on the rows not closed (every row on a calendar or a lanes board, whose\nwindow of time narrows them) \u2014 never one of `filters`. `filters` are the ones a reader\nnarrows by every day \u2014 none to three, never a quota \u2014 in order of use, each a field the\nrows show (a column, a part of the row), each on the line\nwith the search and the chips, the grouping one\nchip at its end; a member filter offers "mine". A date filters by a range of days \u2014 of\nminutes on a `datetime` field, its end not held, so back-to-back shifts read each row\nonce \u2014 and can keep the rows holding no date too (in the yard at a moment: in before it,\nout after it or not yet). A number filters by a range typed in its\nunit or currency \u2014 a figure read in each row\'s own, in the one picked beside it \u2014 with a\nslider over a percent or a figure against a constant `limits`; `{ "field", "tiers" }`\noffers the bands its breakpoints cut in place of a range (`[1000, 5000]` on a weight in\nkg: under 1.000 kg, 1.000\u20135.000 kg, 5.000 kg and over), refused on a figure read in each\nrow\'s own unit. A threshold with a name is a formula yes/no, never a range. A\ncolumn the row already draws is refused. `sort` is one key or a list of keys, most\nsignificant first, each breaking the ties of the ones before it \u2014 a shipping line in its\nselect\'s option order, then the arrival day soonest first:\n`[{ "field": "shipping_line" }, { "field": "arrived_on" }]`; a header\'s press leads them.\nAbsent `sort`, rows open soonest deadline first where the row has a `due`, else newest first. `search` names the fields\nthe search box matches \u2014 a link by its row\'s title \u2014 and a pasted list searches each\nline, naming the lines no row matched; absent, it matches every word of the row.\n`export` saves the register as its report \u2014 the title, a line per chip, the search and\nthe moment it was exported, the readings, and every row in view, at most 20,000 of them.\n`export: true` saves it as a workbook, the readings on a sheet before the rows. A list\nstates the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the\nmoment it was made), `dates` (each date filter\'s `from` and `to` by field, a day or a\nmoment as the filter bounds the rows) and the rows under the entity\'s alias (an html one\nmade a PDF, an `excel` one a workbook; absent, the workbook). A report\'s `filename` names\nits file in the template grammar over the same keys but the rows \u2014\n`"Stock {{dates.arrived.to | format:\\"dd.MM.yyyy\\"}}"`; absent, the template\'s name, or\nthe label. A report of some rows (`where`), or of one value the reader picks (`per`: one\nline\'s file), first narrows the register as its chips, so the screen shows what the file holds.\n`readings` read the register\'s own rows above them: the rows in view, so the search,\nthe status and the filters narrow each one, but a picture\'s own field, which it reads as\nthe register opens on it. They are optional \u2014 state them where a\npicture answers something the rows cannot \u2014 and stand under the title in one compact\nband: lead with the job\'s headline number (a `metric`), then at most the mix (a\n`breakdown`) and the movement (a `trend`) \u2014 a `pivot` stands under the band as a whole table, every line and its totals. A picture\nstands only where it splits the rows in view into two parts or more, one of them\nholding two rows or more; a split whose rows\nall fall in one part, and a lone figure with no picture beside it, are said on the\nsummary line instead, so a band stands only holding a picture or two figures. The summary line already totals the\nfigure over the rows in view and the status chips count them, so a `metric` summing the\nfigure, or counting the rows of an entity with a status, is refused unless its `where`\nnarrows it.\n\n`where` keeps the rows the app works, read so on the server and never a chip: a\npurchase desk over orders of both directions states `"where": { "direction":\n["purchase"] }`, and a row added there is written holding the one option (a select kept\nat several is asked among them). `opens` is the view the register opens on, each entry a\nchip the reader may take away: a cashier opens on `"opens": { "owes": true }`. Both take\na select\'s options or a yes/no\'s value, stored or a formula; `opens` on the status is\nthe chips\' opening choice. `opens` also takes `"me"` on a member field \u2014 `"opens": {\n"assignee": "me" }` opens on the rows assigned to whoever reads, the server reading the\nreader, never the page; `where` refuses it, since it fixes the rows for every reader\n(who may read a row at all is the entity\'s `read_scope`).\n\n`layout: "calendar"` places each row on its day \u2014 a month, a week, a day or a list (a\nphone reads the list or a day): at its hour where the date holds one, over its span\nwhere the row\'s line holds a second date after it, its line\'s other words and the\nregister\'s `columns` after its title. The calendar reads the window in view, and its\ncount and the status chips count that window, opening on every row.\n\n`layout: "lanes"` with `lanes` naming a one-link draws each row of the linked entity as\na lane (a chair, a machine, a room), empty ones too, and each register row as a block\nfrom the first date on its line to the second: over the hours of one day where the first\nholds its hour, else over the days of one day, a week or a month. A block reads the row\'s\nname and the first of its `columns`; a free stretch between blocks adds a row there, its\nlane and its start set, and its end where the next block bounds it. A phone lists each\nlane\'s blocks and free stretches in clock order. Rows naming no lane stand first, in a lane\nof their own; where the app writes the link, each block moves to another lane from its menu.\nInside a folder (`scope`), the lanes are the ones whose own one-link names that folder \u2014 a\nbranch\'s rooms, never every branch\'s. A span\'s end holds its day unless `records` names it\nthe day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each\nblock\'s number and reads it against the lane row\'s own, at most two. A lane holds its\nblocks while they stand, so it reads the most they add up to at one time in the window \u2014\na day\'s rows together \u2014 named, past its bound, by how much and on which day or at which\nhour. The two are in one unit, or two of one dimension (kg against t); a block then reads\nwhat it adds, and the move menu reads each lane as it would stand with the block on it.\nA bound a single row must keep (no parcel heavier than its van) is a `write_rules` `max`\nthrough the link, which refuses the move.\n\n`layout: "roster"` draws the register\'s rows down the side and the days of a week or a\nmonth across; `roster` names the entity whose rows fill the days \u2014 each stands on one\nregister row by its single link to it, and on the first date of its line (a shift on a\nperson\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s status, else the first single select on its line (a\nshift\'s kind), several rows their count; a week\'s cells say their words and one more\nfact of the line, a month\'s keep their marks. A day holding none stays empty: nothing\nwas due, which is never an absence. A filled cell opens its row; an empty one adds a row\nthere, the register row and the day set, asking what any add of it asks \u2014 unless no\nperson writes those rows (`writes: false`). With `expect` (a single select of those\nrows) the columns are its options in place of days \u2014 a checklist of papers \u2014 and `of`\n(this entity\'s multi-select) names the ones each row expects: a gap among them reads as\none and adds its row, the others stand blank, and each row reads what it holds of them,\n`2/3`. A roster states no `columns` and needs no `readings`: its legend counts each\nstate in view.\n\n```jsonc\n"register": { "layout": "roster", "roster": "run", "filters": ["operator"] }\n```\n\nAn app whose job reads its rows otherwise than the entity\'s `records` restates them in\nits own `row` \u2014 the cashier\'s figure is what is still owed, its line the service and the\nday. The subtitle, figure and image it states replace the entity\'s wherever that app\ndraws a row of it; every other app reads `records`, and the title is the entity\'s.\n\n### Readings\n\nA reading is one number, or one picture of numbers, over rows. Where it stands decides\nwhich rows: a dashboard reads every row, a register\'s `readings` the rows in view, a\nrecord\'s reading block the child rows under the record. The picture is the runtime\'s,\nchosen from what the reading means \u2014 no key picks a chart.\n\n```jsonc\n{ "metric": "visit", "value": "fees", "where": { "paid": false }, "over": "arrived_on", "label": "Unpaid" }\n{ "breakdown": "visit", "by": "line" }\n{ "trend": "visit", "over": "arrived_on", "value": "fees" }\n{ "pivot": "visit", "rows": "size", "columns": "line" }\n{ "list": "visit", "where": { "stage": ["in_yard"] }, "columns": ["line"], "app": "gate", "label": "In the yard" }\n{ "list": "task", "where": { "assignee": "me" }, "label": "My tasks" }\n```\n\nA reading\'s `where` keeps rows by a select\'s options, a yes/no\'s value, or `"me"` on a\nmember field \u2014 the rows that name whoever reads, read so on the server.\n\n<!-- generated:start apps-readings -->\n\n#### Metric\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Breakdown\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n\n#### Trend\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n\n#### Pivot\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n\n#### List\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `list` | alias | yes | The entity whose rows are listed, each in its `records` anatomy |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `columns` | list of alias | no | Fields read after each row\'s title |\n| `app` | alias | no | An app of this model over the same entity that a row opens in |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n<!-- generated:end apps-readings -->\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A reading sums a number, never a percent \u2014 percents do not add up. |\n| `reading.split` | `by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 and a pivot crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s `where` keeps rows by a select\'s options, a yes/no\'s `true` or `false`, or a member field holding the reader (`"me"`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A list\'s `app` is a register app of this model over the list\'s own entity. |\n\n<!-- generated:end rules-reading -->\n\nA reading counts its rows, or sums the number `value` names \u2014 never a percent. `by`,\n`rows` and `columns` split the rows by one value each: a single select, a one-row link,\none member, or a yes/no \u2014 stored or a formula, its parts read as its label and the\nothers. `over` is a date, the row\'s own (stored or a formula) or its parent\'s through a\nlookup over a one-link: a trend\'s periods, a metric\'s recent periods beside its number,\nand what a dashboard\'s period windows. `ahead: true` reads a trend forward from the\ncurrent period over the rows still to come. `where` keeps rows whose select holds one\nof the options, or whose yes/no is the value, every entry ANDed \u2014 a band leads with the\njob\'s exception as a formula\'s yes/no (what is late, what is short), never a stage the\nstatus chips already count. A metric summing a value `limits` bounds reads against the\nsum of its bound (`10 / 14`); `target` reads one against a number nothing bounds. A\nmetric\'s change is green where it moves the good way: a sum\'s rise, a count kept by\n`where` falling; `better` states the other way where that reads wrong \u2014 money going out\n(`"better": "down"`). A register reads only its own entity; a `list` stands only on a\ndashboard.\n\n### Record\n\n```jsonc\n"record": {\n "door": "page",\n "sections": [\n { "title": "Details", "fields": ["size", "line", "built_on"] },\n { "title": "Gate in", "at": ["arriving", "in_yard"], "fields": ["arrived_on", "truck"],\n "blocks": [{ "rows": "fee", "where": { "leg": ["in"] }, "expect": "kind", "columns": ["amount", "paid_by"] }],\n "acts": ["gate_in_paper"] },\n { "title": "Sell to a buyer", "description": "Moves the container to the buyer\'s stock once it has left.", "acts": ["sell"] }\n ]\n}\n```\n\n<!-- generated:start apps-record -->\n\n#### Record page\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` | no | A page for work read at length; a drawer for rows worked one after another from the register. Either stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page` or the record has more than three sections, else a drawer |\n| `sections` | list of [Section](#section) (at least one) | no | The record in the order its work reaches it \u2014 each section its fields, then its blocks, then its acts. Absent, one section of every field a person writes that no other place shows |\n| `comments` | `true` | no | The record\'s comment thread beside the sections \u2014 who wrote what and when, with a composer \u2014 stated only where the job\'s people discuss a record or the owner asks for one. Absent, it is drawn nowhere |\n| `history` | `true` | no | The record\'s status history beside the sections \u2014 each move, when and who \u2014 stated only where the owner asks for an audit trail of the moves: the rail already marks the steps passed, and the entity\'s `status.history` is written either way. Absent, it is drawn nowhere |\n\n#### Section\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | text | yes | What the section is, in the reader\'s words \u2014 its heading, and its name on the record\'s rail or tab |\n| `description` | text | no | One sentence under the title: what the section is for, or what its act does |\n| `at` | list of alias (at least one) | no | Options of the record\'s status (or its milestones) during which this section is the work now \u2014 a step of the path the record moves through: its rail item reads now while the status holds one and this app has work there, done once the record\'s history says it went through one \u2014 or it is a step of the path before where that history opens (with none, where the record stands) that no act moves the work past \u2014 and it stands before the section that is the work now, and waiting otherwise; a section every option of which ends the work, past the path\'s end or entered by its own act, is an exit: no item and nothing drawn until the record stands in it, then set apart after the steps and never one; A page opened from the register opens at it. It never locks what the section holds: its fields are written as the write rules and checks allow at any stage. An act it names is offered during these alone: its `when` holds the status to them, or on milestones it stamps the step after one of them \u2014 a detour and an exit are sections of their own, a detour stated before the step it returns to |\n| `fields` | list of alias (at least one) | no | The record\'s own fields in this section, in the order they are read \u2014 never one the header draws: the row\'s title, subtitle, image, status or figure |\n| `blocks` | list of [block](#blocks) (at least one) | no | What the section holds besides its own fields, under them, in order |\n| `acts` | list of alias (at least one) | no | Acts of this app drawn under the section\'s fields, their asks as more of them and what each lacks said above its button; the header draws no act, so every act of the record itself is named by one section \u2014 an act of a child (`of`) stands on the child\'s row and is never named here. An act moving the status, offered at ONE step (the stage its `when` holds), stands at that step\'s foot when it moves the work forward \u2014 to a later option, an exit too \u2014 or back to a step the work passes anyway; one offered at several steps, or moving back into a detour, stands in the section it enters, which draws it while the work waits \u2014 an exit\'s in the header\'s \u22EF, since an exit stands only once the record is in it. A correction \u2014 `danger` and not one of its step\'s outcomes (no other status move offered at the same moment) \u2014 waits in the section heading\'s \u22EF, and one no section names in the header\'s \u22EF; every other act is named by a section |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `page` | `true` | no | A page of its own, off the record\'s scroll: its own heading and a way back, reached from the record\'s rail after the scroll\'s sections, or from a link at its place where the record has no rail \u2014 for what makes the record worse stacked in it: a long list of related rows, a workspace of its own. Only on a page door, never a step (`at`) |\n\n<!-- generated:end apps-record -->\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A field, a block and an act each stand once on a record: the header draws the title, subtitle, image, status (or milestones), figure and folder, and a field\'s one other place is a section\'s `fields`, a files block or a text block \u2014 save the image, which one files block may draw again, whole. |\n| `record.foot-total` | A rows block\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also one of a section\'s `fields`. |\n| `record.fact-restates` | A section\'s field never looks up what its link already reads where it stands: the linked row\'s title and line beside the link, the figure or bound its meter reads on the header\'s line. |\n| `record.section-holds` | A section holds fields, blocks or acts. |\n| `record.at` | `at` names options of the record\'s stored status, or its milestones; a record with no status has no steps. |\n| `record.page` | A section is a `page` only on a page door and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section. |\n| `record.history` | `record.history` draws a status history the entity records (`status.history`); no block reads that history. |\n| `record.section-acts` | A section\'s `acts` name acts of the record itself: an act of a child (`of`) stands on the child\'s row, and one over the register\'s rows (`on`) on the register. |\n| `record.act-in-section` | Every act of the record stands in the `acts` of the section whose work it is; only a correction no section owns \u2014 `danger`, and no other status move offered at the same moment \u2014 waits in the header\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the section `at` the status it enters. |\n| `record.act-staged` | An act at a staged section\'s foot is offered only during that section\'s steps: its `when` holds the status to options the section names \u2014 on milestones, it stamps the step after one of them. |\n| `record.act-hidden` | An act is never offered only while its section\'s `when` hides the section. |\n\n<!-- generated:end rules-record -->\n\nThe record is its `sections`, stacked in the order the work reaches them. A section is\nits fields, the acts under them, then its blocks \u2014 the acts at its foot instead where one\nreads what a block holds or none of its fields \u2014 and holds at least one of them. From\nthree sections on, the record is navigated by their titles: a page by a rail that jumps\nto each (a strip of the same names on a phone), a drawer by tabs, the companion the last;\nwith fewer, the sections just stack. `description` is one sentence under the title.\n`at` names the status options (or the milestones) during which the section is the work\nnow \u2014 only on a record that truly moves through those stages: the rail marks it done,\nnow or waiting (an item of no `at` is its title alone), and the section stands even holding\nnothing yet \u2014 it never hides a field, locks it or gates an act, and a record opens at its\nhead. `page: true` makes a section a page of its own under the\nrecord\'s, off its scroll, with a way back \u2014 rarely, for a long related list or a workspace\nof its own that would make the record worse stacked; never on a drawer or a step. The\nheader draws no act: every act on the record is named by the section whose work it is, and one no section names is refused, naming its likely\nsection \u2014 only a correction no section owns waits in the header\'s \u22EF beside Delete. An act\nmoving the status and offered at ONE step (its `when`) stands at that step\'s foot when it\nmoves the work forward \u2014 to the next step, or to an exit later in the status\'s options\n(Reject beside Approve) \u2014 or back to a step the work passes anyway. One offered at SEVERAL\nsteps, or moving back into a detour, is named by the section it enters: a detour draws it and its\ngaps alone while it waits. A `danger` act is a correction \u2014 waiting in the \u22EF of what it acts\non: its section\'s heading, its row, the header\'s where no section names it \u2014 unless it moves\nthe status while another status move is offered at the same moment; then it is one of that\nstep\'s outcomes, a button. Cancel offered at Held and at Confirmed, beside Confirm, is an\noutcome into an exit: state `{ "title": "Cancelled", "at": ["cancelled"], "fields": ["reason"],\n"acts": ["cancel"] }`. An exit stands only once the record is in it, its reason there and its\nrail item apart from the path; until then Cancel waits in the header\'s \u22EF. `comments: true`\nputs the record\'s comment thread beside the sections, and `history: true` its status history \u2014\nthe history never as a section. Both are opt-in: state them only where the job\'s people\ndiscuss a record, or the owner asks for an audit trail of its moves.\n\nA field, a block and an act is each placed ONCE on a record \u2014 the header, one section;\na second place is refused. The header draws the entity\'s `records` title, subtitle,\nimage, status (or milestones) and figure, so none of them is named again in a section\'s\n`fields` or the register\'s `columns` \u2014 save the image, which one `files` block may draw\nagain, whole at reading size (an incident\'s photo, a scan), the header keeping its mark. Refused too: a section holding nothing, an `at`\non a record with no status, and a section\'s act that is a child\'s or runs over the\nregister\'s rows.\nAbsent `sections`, one section holds every field a person writes that the header does\nnot show. An editable field rests as its control and saves as it changes; everything\nelse is plain text. A section or a block with `when` is shown only while each named\nselect of the record holds one of its options. The one layout serves every entity: a\ncustomer or a product is sections without `at`.\n\n### Blocks\n\n```jsonc\n"blocks": [\n { "rows": "fee", "columns": ["kind", "amount"] },\n { "rows": "fee", "where": { "leg": ["out"] }, "columns": ["kind", "amount"], "under": "invoice" },\n { "agenda": "booking", "columns": ["guide"], "start": "arrival" },\n { "timeline": "note" },\n { "files": ["photos", "papers"] },\n { "text": "remarks", "when": { "stage": ["held"] } },\n { "metric": "fee", "value": "amount", "label": "Fees" },\n { "breakdown": "fee", "by": "kind" },\n { "trend": "fee", "over": "charged_on", "value": "amount" },\n { "pivot": "fee", "rows": "kind", "columns": "method" }\n]\n```\n\n<!-- generated:start apps-blocks -->\n\n#### Rows block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | A child entity whose rows belong to this record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure\'s meter reads against), though an `expect` block\'s figure is a column, filled in place |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `where` | map of alias \u2192 list of alias (at least one) | no | Only the child rows whose single select holds one of these options \u2014 every entry ANDed, read so on the server; a row added here holds them (one leg\'s fees, of a record holding both legs\'). Each select is one the child requires, so no row stands in no block |\n| `expect` | alias | no | The child\'s title when the record holds one row per value of it \u2014 a single select (one per option, in order) or a one-row link to a catalog (one per row the link may point at): a value not yet filled reads as its own empty line, and no value holds two rows. The block\'s `columns` name at least one field a person fills in place |\n| `of` | alias | no | With `expect` on a select: the record\'s own multi-select holding the options this record expects a line for \u2014 the options it holds, in order, rather than every option. Its options are among the title\'s |\n| `under` | alias | no | The child\'s one-link to a document that covers its rows (an invoice over its fee lines), whose many-link points back and which links to this record: each document stands as a heading over the lines it covers; a filled line no document covers says so on its own row with a make for one document over it, and two or more waiting take a combined make in the block\'s heading. Never also a rows block of the document |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Timeline block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `timeline` | alias | yes | A child entity read as a log of dated entries, newest first, with a composer where the app adds entries, each corrected and removed where it stands as a rows block\'s row (entries planned ahead are an `agenda`) \u2014 never the record\'s status history, which `record.history` draws beside the record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, the subtitle and figure no default fills, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Agenda block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `agenda` | alias | yes | A child entity read as planned entries on a time axis: by day, soonest first, each entry with its picture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or subtitle); within a day entries follow a datetime there, else the first single select after the date \u2014 its options in the day\'s order |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s facts read on an entry\'s line after its title \u2014 never what its line already draws (subtitle, image, status, figure) |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it (Day 1, Day 2); absent, the weekday and the date alone |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Files block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `files` | list of alias (at least one) | yes | Files fields of this record, read as pictures and documents |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Text block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `text` | alias | yes | A long text field read as prose |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Metric block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Breakdown block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Trend block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Pivot block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n<!-- generated:end apps-blocks -->\n\n<!-- generated:start rules-block -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.child-link` | A block reads a child linked to the record; `via` names the link where the child links more than once. |\n| `block.column-drawn` | A child block\'s column never names what the child\'s row draws \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 nor its link back to the record, and names a field once; an `expect` block\'s figure is its filled-in-place column. |\n| `block.title-back` | Where a child\'s title is its link back to the record a block lists it under, its first other subtitle names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `block.where` | A rows block\'s `where` names single selects the child requires, by options they hold; one held at a single option is never also a column. |\n| `block.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `block.under-once` | Documents drawn over their lines (`under`) are never listed again as a block of their own. |\n| `block.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the block; the block\'s `columns` name at least one field a person fills in place, and its `create` takes the file where the child is pictured by one. |\n| `block.expect-fills` | An expected line is made from its key and its first filled column, so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `block.expect-of` | A block\'s `of` needs `expect` on a select, and names the record\'s multi-select whose every option is one of the title\'s. |\n| `block.agenda` | An agenda\'s child names a date on its row line (`records` title or subtitle); `start` is a date of the record. |\n| `block.timeline` | A timeline\'s child holds a date to place its entries by. |\n| `block.field-kind` | A files block names files fields of the record, and a text block a text field. |\n| `block.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `block.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: an `agenda` draws them by day. |\n| `block.under-note` | *Noted, never refused:* Rows holding a paper that cover another block\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n\n<!-- generated:end rules-block -->\n\nA `rows`, `agenda` or `timeline` block reads a child entity through its link to the\nrecord \u2014 `via` names it where the child links more than once. A rows block adds rows\n(asking the child\'s title, its picture, the block\'s columns and its required fields)\nand opens each in a drawer. An `agenda` draws planned entries by day, soonest first:\nthe first date on the child\'s row line is the day, a datetime there or the first single\nselect after the date orders a day, and `start` \u2014 the record\'s date \u2014 is day 1. `under`\nnames the child\'s one-link to a document covering its lines (an invoice over its fees,\nits many-link paired back, linking to the record): each document heads the lines it\ncovers, the uncovered lines first \u2014 the record never lists the documents again as a\nblock of their own. A filled line no document covers says so on its own row, where one\npress makes a document over that line alone, its figure starting at the line\'s amount;\nwhere two or more wait, the block\'s heading makes one over those ticked in its dialog.\nA timeline reads its child\'s first date as the moment and its member field as who, with\na composer where the app writes it. An agenda\'s or a timeline\'s entry with no day yet is\na plan, listed first and dated in place. `create` names the child\'s fields an add here asks \u2014\nthe link to this record is filled, never asked \u2014 and `false` adds none. `expect`\nnames the child\'s title where the record holds one row per value of it \u2014 a select\'s\noptions, or the rows a link to a catalog may point at (`options_where`): every value\nis a line, one not yet filled a quiet line whose first column makes its row \u2014 or, where\nthe child is pictured by a file a person brings (its `records` `image`), a placeholder\nnaming the paper whose upload makes the row with its file \u2014 the add\noffers only the values not yet held, and every write path refuses a second row of one\nvalue under one record. A field that starts from the catalog row (`default_from`)\nshows that value as its hint, taken with one press. `where` keeps only the child rows\nwhose single select holds one of its options \u2014 read so on the server, a row added\nthere holding them, its lines expected once per value among them (one leg\'s fees\nbeside the other\'s), and its foot totalled by the record\'s sum narrowed alike. A `metric`, `breakdown`, `trend`\nor `pivot` block is a reading (\xA7 Readings) of a child entity\'s rows under this record,\nread only \u2014 `via` names the link where the child has more than one.\n\n### Tasks\n\nAn entity whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is\nwork someone finishes. Every table of its rows draws it alike \u2014 a rows block of the record it\nbelongs to, its own register, the rows filed under one of its rows (its steps) \u2014 each row led by a\nring. Ticking the ring moves the row to a closed option by the one write the app already has for\nthat move \u2014 the act of that entity setting a closed option (its first preferred), offered on an\nopen row and asking nothing, else its own save where people write the status \u2014 stamping what\nthat act stamps and appending the status history. Unticking runs the act moving a closed row\nback (its `when` holding a closed option), else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled, never hidden, and the check notes it. The status is moved\nin place on the row by the same moves; every other field is edited where the row opens. A\nfiles column reads on a task\'s line as how many files it holds. An act `of` the child with\n`on: "rows"` completes several at once: the list\'s heading offers to select rows, and the act\nruns on those picked. A block with `expect` or `under` draws its own lines, never rings.\n\n```json\n{\n "entities": [\n {\n "alias": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "lead", "label": "Lead", "type": "select_member" }\n ]\n },\n {\n "alias": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": [\n { "alias": "title", "label": "Title", "type": "text", "required": true },\n {\n "alias": "project",\n "label": "Project",\n "type": "select_record_link",\n "target_entity": "project",\n "cardinality": "one",\n "required": true\n },\n { "alias": "assignee", "label": "Assignee", "type": "select_member" },\n {\n "alias": "status",\n "label": "Status",\n "type": "select",\n "required": true,\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ],\n "default": ["todo"]\n },\n { "alias": "due", "label": "Due", "type": "date", "format": "date" },\n { "alias": "done_on", "label": "Done on", "type": "date", "format": "date" }\n ]\n }\n ],\n "records": {\n "project": { "title": "name" },\n "task": {\n "title": "title",\n "subtitle": ["assignee"],\n "status": { "field": "status", "closed": ["done"] },\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" }\n }\n },\n "apps": [\n {\n "alias": "projects",\n "name": "Projects",\n "entity": "project",\n "record": {\n "sections": [{ "title": "Work", "fields": ["lead"], "blocks": [{ "rows": "task", "columns": ["due"] }] }]\n },\n "acts": [\n { "alias": "complete", "label": "Complete", "of": "task", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "reopen", "label": "Reopen", "of": "task", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "alias": "complete_all", "label": "Complete", "of": "task", "on": "rows", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } }\n ]\n },\n {\n "alias": "my_tasks",\n "name": "My tasks",\n "entity": "task",\n "register": {\n "opens": { "assignee": "me" },\n "readings": [{ "metric": "task", "where": { "status": ["todo"] }, "label": "Open" }]\n },\n "record": { "door": "drawer" },\n "acts": [\n { "alias": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n}\n```\n\n### Acts\n\n```jsonc\n"acts": [{\n "alias": "gate_out", "label": "Gate out",\n "when": { "stage": ["in_yard"] }, "requires": ["release", "seal"],\n "asks": ["seal", { "input": "note", "label": "Note", "type": "long_text" }],\n "set": { "stage": "gone", "left_on": "now", "left_by": "me", "remark": "input:note" },\n "confirm": true\n}]\n```\n\n<!-- generated:start apps-acts -->\n\n#### Act\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The act\'s name within its app \u2014 its workflow is `act_<alias>` |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"record"` \\| `"rows"` \\| `"view"` | no | `rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked in the record\'s task list; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row\'s menu) |\n| `of` | alias | no | A rows block\'s child entity the act works on: offered on each child row \u2014 or, `on: "rows"`, on the rows ticked together in a task list \u2014 its conditions and its write that row\'s |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The act works only while each named select holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands (its section\'s fields, a block\'s, or the act\'s asks \u2014 never the header alone); a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| [Act input](#act-input)) (at least one) | no | What the reader states when pressing it \u2014 fields of the record (written by the act) or inputs |\n| `set` | map of alias \u2192 (text \\| number \\| boolean \\| `null`) | no | What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"` |\n| `workflow` | alias | no | An authored workflow (src/workflows/<alias>.ts) the act runs after its checks, for what `set` cannot say |\n| `template` | alias | no | A document template the act makes a file from \u2014 over several rows, one file of them all, the rows listed under the entity\'s alias |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into` (a case\'s forms), instead of one `template`: drawn where the act stands as a list of its papers \u2014 each made one as its file, each not yet made as a placeholder naming it \u2014 the reader ticks which to make, and one press makes those, each replacing the paper its template made before |\n| `into` | alias | no | The files field of the one record the made document is kept in |\n| `intake` | alias | no | A child the record lists in a rows block whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child \u2014 its kind the child\'s title, its file in the child\'s files field |\n| `record` | [Act record](#act-record) | no | The press records a call, visit or meeting through the host \u2014 Stop ends it \u2014 and files it as a new row of a child the record lists: its audio, its screen where captured, its transcript |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link of the record names, or a child whose rows the papers add \u2014 shown before and after, and saved where it differs. With `record`, fields of the new row an agent fills from the transcript, saved as they come |\n| `confirm` | `true` | no | The press asks first; the question lists every active warning |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Act input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` | yes | What the reader states |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Act record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n<!-- generated:end apps-acts -->\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.set-not-asked` | A field an act `set`s is the act\'s to write: read-only everywhere else and never asked by an add (`create`) \u2014 a required one takes a `default`. |\n| `act.of` | An act\'s `of` names a child the record lists in a rows block; it works on the one child row it is pressed on \u2014 or, `on: "rows"`, on the rows ticked together where the record lists that child as tasks \u2014 never on the rows in view. |\n| `act.writes-line-key` | An act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.view` | An act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an act `requires` is drawn where the act stands \u2014 its section\'s fields, a block\'s, its asks, or an unstaged section\'s where its own is unstaged \u2014 never the header alone. |\n| `act.requires-written` | A field an act requires that only an act writes is written by an act offered together with it, or asked by the act itself. |\n| `act.recommends` | A field an act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `"now"` on a date, `"me"` on a member, `"input:<name>"` of an input the act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.set-milestone` | An act stamps a milestone, never clears one \u2014 unticking clears it and every later one. |\n| `act.into` | `into` is a files field of the one record the act is pressed on, beside its `template` or `templates`. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template`, `asks`, `of` or an `on` over several rows. |\n| `act.history-copy` | What a status move asks is copied onto the history\'s field of the same name, which holds the same kind of value. |\n| `act.intake` | `intake` names a child the record lists in a rows block that expects its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline block or a rows block with no `expect`, `under` or `where`, linking back by one single link, whose row nothing requires beyond what the recording writes: `audio` and `video` its files fields, `transcript` a text field, `at` a date, `by` a member field, each a field of its own; the act runs on one record and states nothing but its `label`, `when`, `requires` and `fills` beside it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`, each is a field of the recording\'s row that the recording itself does not write. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link of the record names, or a child the record adds rows of in a rows block with no `expect`, `under` or `where` \u2014 its rows filled in the fields that block adds them with, among them every field an added row cannot be made without. Each field filled is one a person writes holding text, a number, a date, a yes/no or options, never a status, a milestone or a field an act writes. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. |\n\n<!-- generated:end rules-act -->\n\nAn act whose `when` does not hold is not offered \u2014 each select named holding one of its\noptions, each yes/no named (stored, or a formula: a period that has ended) its `true` or\n`false`; every one that holds stands at the\nfoot of the section naming it in `acts` \u2014 a correction in that section heading\'s \u22EF, one\nof a child row on its row (a correction in the row\'s \u22EF). An act\'s `requires` are fields\nits section draws \u2014 its `fields`, a block\'s, the act\'s `asks`, or those of another section\nwith no `at` where its own has none; one shown only in the header or a staged section\nelsewhere is refused. One whose `requires` are\nempty is offered disabled, naming each; one whose `recommends` are empty names what\nit will leave blank and still presses; one a standing check blocks is offered\ndisabled in the check\'s words. An input\'s `default` is the row\'s field it starts from. The generated `act_<alias>` workflow re-checks all three on the\nserver, writes `set` and what was asked, and appends the status history where the\nact moves the status \u2014 each thing it asked copied onto the history row where the\nhistory has a field of the same name. `template` makes a document from the row and\nkeeps it in `into`; over several rows it makes one document of them all, the rows\nlisted under the entity\'s alias, and hands it back. `templates` instead names a set of\npapers kept in `into`, listed where the act stands \u2014 each made one as its file, each\nnot made yet as a placeholder \u2014 those whose `when` holds ticked to start; the press\nmakes the ticked ones (sent in `templates`), each replacing the file its template made\nbefore (the file whose `document_template_id` is that template \u2014 never a file a person\nput there), and the set downloads as one archive (`archive_<alias>`). A field an act writes is read-only everywhere else, never asked by an add \u2014 a required one takes a `default`. A `select` input offers its own options and is written into\na select holding the same option aliases; a `link` input picks one row of its `entity`\nand a `member` input one person, each written into a field of its kind. A field an act\nboth `requires` and `asks` never disables the press \u2014 the act asks it, required.\n\n`on: "view"` runs on every row the register shows \u2014 its search, status and filters \u2014\neach row checked before any is written. `of` names a rows block\'s child entity: the\nact is offered on each child row (its menu and its drawer), its `when`, `requires`,\n`set` and checks are the child\'s, it runs on that row, and a check of the record that\nlocks the child rows locks it too. With `on: "rows"` it runs on the child rows ticked\ntogether in the record\'s task list (\xA7 Tasks) \u2014 every row checked before any is written;\nit is refused where the record lists no tasks of that child.\n\n`intake` reads papers: the press takes files, an agent (`act_<alias>` among the app\'s\nagents) reads them, and each is filed as the record\'s line of its kind in the named\nchild \u2014 that child\'s title \u2014 or a new line, its file in the child\'s one files field.\nWhat they state of each field `fills` names is shown beside what the row holds; the save\nwrites only what differs \u2014 the record\'s fields, `<link>.<field>` on the row a required\none-row link names, a child\'s rows (one found by the child\'s `natural_key` changed, else\nadded) \u2014 each refused as its own save is, and nothing written while one refuses.\n\n```jsonc\n{ "alias": "read_papers", "label": "Read papers", "intake": "customer_paper", "fills": ["phone", "contact.email", "branch"] }\n```\n\n### Checks\n\n```jsonc\n"checks": [{ "field": "release_warning" }, { "field": "unpaid", "blocks": ["gate_out"] }, { "field": "locked", "blocks": ["edit"] }]\n```\n\n<!-- generated:start apps-checks -->\n\n#### Check\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the record (or of the child `of` names): true or non-empty means the check stands; its label is its short name on a row\'s line, and a text formula\'s words are the sentence the record and a tooltip say |\n| `of` | alias | no | A rows block\'s child entity this check is a formula of: standing on a child row, it refuses what it blocks of that row |\n| `blocks` | `"all"` \\| list of (alias \\| `"edit"` \\| `"delete"`) (at least one) | no | Acts, "edit" (the saves and the Delete), "delete" (the Delete alone) or "all" that the standing check refuses; absent, it only warns |\n| `resolve` | alias | no | An act of this app, on the rows the check stands on, that clears it: the check\'s line links to the act where it stands, and the act keeps its own conditions |\n\n<!-- generated:end apps-checks -->\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A check\'s `field` is a yes/no or text formula of the record, or of the child `of` names. |\n| `check.of` | A check\'s `of` names a child the record lists in a rows block. |\n| `check.not-placed` | A check\'s field is never also placed on the record \u2014 the line under the header says it. |\n| `check.blocks` | A check of the record blocks its acts, `"edit"`, `"delete"` or `"all"`; a check of a child blocks that row\'s saves, its Delete, the acts `of` that child, or all \u2014 never the record\'s acts. |\n| `check.resolve` | `resolve` names an act of this app on the rows the check stands on, pressed on one row, not refused by the check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\nA check is a yes/no or text formula of the record: it stands while true or\nnon-empty, and a text formula\'s words are what the reader sees. Without `blocks`\nit warns; with them it refuses the acts named, `"edit"` the record\'s own saves (and\nits Delete), `"delete"` its Delete alone, or `"all"` \u2014 on the server, in every write it\nnames. A check `of` a rows block\'s child stands on each child row: it refuses that\nrow\'s saves (`edit`), its Delete (`delete`), the acts `of` that child it names, or\n`all` of them \u2014 never the record\'s acts. A warning reading the figure of a meter that\n`gates` marks against its bound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s\nline says it in the check\'s tone (the most severe of several, and how many more), while\nits words whole are the row\'s tooltip and stand under the record\'s header. So label it\nas the reader names the problem, in a few words, and let a text formula say the\nsentence. The compiler records the fields a formula reads: a row drawing each of them\nas a control says nothing of the check on its line (the empty control says it), and a\ncheck with no `resolve` links to the first of them the record draws.\n\n## What a run remembers\n\nThe WORKSPACE remembers what each entity, field, select option, role and\ntemplate alias became here, plus which record each first row landed on and the\nfile each document path was uploaded as. The model file itself holds no live id\n\u2014 it is the portable half, and the same file applies to a demo workspace and to\na customer\'s \u2014 so the join lives where the things it names live, and one model\napplied to two workspaces holds two bindings that know nothing of each other.\n\nIt is the workspace\'s and not the file\'s because a model file is never\ncommitted: memory kept beside it is one author\'s disk, absent for a teammate, on\na second machine or after a delete \u2014 and every one of those goes quietly back to\nbinding by label, which is what grows the second table.\n\nEvery later apply binds through it: it takes the remembered id first and falls\nback to a label only for an alias nothing has bound \u2014 a field you have just\nadded, or a workspace nothing has applied this model to. That is what makes a\nrelabel on EITHER side a rename rather than one thing the workspace lacks and\none the model lacks: `apply` binds the thing it has always meant and reports the\nmove \u2014 which side is right is yours to decide, not a reason to refuse the run.\n\nA bound target the workspace no longer holds IS a refusal, by name: re-binding\nto whatever carries that label today is how the model comes to point at\nsomebody else\'s table. `lotics run restore_table` puts a deleted table back.\n\n`lotics model pull` writes a workspace that already works as a model file \u2014 the\nstarting point for another business\'s model, never a source of truth: it carries\none business\'s words and stops describing that workspace the moment either\nchanges.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "logo", "label": "Logo", "type": "files" },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "format": "currency",\n "currency": "VND"\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "format": "currency", "currency": "VND" }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n },\n { "alias": "note", "label": "Note", "type": "text" }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "records": {\n "customer": { "title": "name", "image": "logo", "party": "organization", "figure": "total_ordered" },\n "order": { "title": "customer", "subtitle": ["code", "placed_on"], "figure": "amount", "starts": { "placed_on": "today" } }\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "entity": "customer",\n "register": {\n "columns": ["tier"],\n "readings": [{ "breakdown": "customer", "by": "tier", "value": "total_ordered" }]\n },\n "record": {\n "sections": [\n { "title": "Account", "fields": ["tier"] },\n { "title": "Orders", "blocks": [{ "rows": "order", "columns": ["total"], "create": ["code", "amount"] }] }\n ]\n }\n },\n {\n "alias": "orders",\n "name": "Orders",\n "entity": "order",\n "register": {\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "readings": [{ "trend": "order", "over": "placed_on", "value": "amount" }]\n },\n "record": { "door": "drawer" }\n }\n ],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\nEach app of this file, as its reader will see it:\n\n```\nApp customers \u2014 "Customers" over Customers (customer)\n Register \u2014 table (default), newest first (default)\n row [Logo] \xB7 Name \xB7 figure Total ordered\n columns Tier\n filters \u2014\n readings "Tier" \u2014 Total ordered summed per Tier, over the rows in view (default)\n add Name (default)\n Record \u2014 a drawer (default)\n header image Logo \xB7 title Name \xB7 figure Total ordered\n checks \u2014\n header \u22EF Delete\n sections 1. "Account"\n fields Tier\n 2. "Orders"\n block Orders (default) \u2014 rows of Orders through Customer; columns Total with VAT; add asks Order no., Amount\n thread \u2014\n history \u2014\n acts \u2014\n Reads Customers, Orders\n\nApp orders \u2014 "Orders" over Orders (order)\n Register \u2014 table (default), newest first (default)\n row Customer \xB7 under it Order no., Placed on \xB7 figure Amount\n columns \u2014\n filters Customer\n readings "Amount" \u2014 Amount summed per period by Placed on, over the rows in view (default)\n add Order no., Customer, Placed on \u2014 starting Placed on at today\n Record \u2014 a drawer\n header title Customer \xB7 subtitle Order no., Placed on \xB7 figure Amount\n checks \u2014\n header \u22EF Delete\n sections 1. the record\'s details (default)\n fields Note\n thread \u2014\n history \u2014\n acts \u2014\n Reads Orders, Customers\n\n```\n\nEach `(default)` is a value the model left to the system: the door a record opens\nthrough \u2014 a drawer, since neither record has a stage or more than three sections \u2014 the\norder rows open in, what an add asks, and the order\'s one section \u2014 every field a\nperson writes that the header does not already show. The customer\'s orders\nare a block in its "Orders" section, never its own `Orders` link as a field as well:\none fact, one place. An order is titled by its customer and read by its number under\nit; the block names what its add asks, since a subtitle or a figure is never asked by\ndefault. `Total ordered` is a rollup, so it is read and never asked; the\norders app\'s record opens in a drawer, because its rows are worked one after another.\n';
40145
+ var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing a workspace: its tables, fields, options, views, roles,\nfirst rows, how a row of each table is recognised, and the apps over them. Each \xA7\nis its own page at `model/<section>` \u2014 `lotics docs`, or the `docs` tool. Complete models of several trades, to read as\nworked examples: `https://lotics.ai/presets/index.json`.\n\n**What a model composes with**\n\n- **Entities and fields** (\xA7 Entity, \xA7 Field) \u2014 the tables, their columns, options and links.\n- **Records** (\xA7 Records) \u2014 how a row of each entity is RECOGNISED: its title, the line\n under it, its picture, its status, the one number it stands for. Stated once per entity\n and read by every surface that draws one of its rows.\n- **Write rules** (\xA7 Write rules) \u2014 what a write finds, copies, bounds and picks among.\n- **Apps** (\xA7 Apps) \u2014 one register over one entity and the record each row opens: which\n fields go where, the acts, and the checks that guard them \u2014 or a dashboard of readings.\n How each thing LOOKS is the runtime\'s, one treatment per concept; no key here changes it.\n A screen the job needs and no key states is a `lotics report` \u2014 the job, what the model\n drew, the word wanted \u2014 never keys bent to approximate it.\n\n**The working order**\n\n1. Name the people and each one\'s JOB \u2014 the work they alone decide or write.\n2. The entities and fields those jobs touch, and a `records` entry for every entity an\n app lists, opens or picks.\n3. One app per job in `apps[]`, each one register over one entity.\n4. `lotics model apply model.json` \u2014 the file is checked first, every problem in one run;\n then the tables, then a new version of every app, live\n (`lotics setup model.json --email you@company.com` where no account exists yet).\n5. Change the file and apply it again; `lotics model pull` writes what the workspace holds.\n Rolling an app back (`lotics run rollback_app`) restores its earlier version \u2014 table\n changes and data writes stay.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not applied \u2014 apply the rest in a second call.\n- **An existing table is adopted.** `lotics model apply` binds an entity whose `label`\n already names a table in the workspace, and adds the fields, options and\n views it is missing. No stored value is ever changed or deleted: a field it\n adopts takes the model\'s `default`, and the `format` and `unit` it reads in,\n where that only relabels \u2014 a date\'s format and a unit converting its figures are\n left. Applying the same model twice changes nothing the second time.\n- **Renaming a field is `lotics run update_table`**, then the same label in this\n file; deleting is `lotics run delete_table`. Neither goes through the file\n (\xA7 What a run remembers).\n- **The file\'s own majority is the language.** A model names no locale \u2014 which\n language it is in is what it mostly says, and `model apply` notes the label\n written the other way. The generated screens read the kit\'s pack, and a\n generated WRITE cannot: its refusals run on the server, so they are worded in\n that same majority. Mix the two and the workspace answers in two languages.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere: sample rows landing among a\n customer\'s real ones cannot be told apart from them.\n- **`lotics model apply` checks all of it before anything is written**, and\n reports every problem in one run rather than the first. Each section of this\n page ends its keys with the rules the check enforces there, one sentence each\n under an id; a finding names its rule\'s id (`[register.filter]`), and\n `model/<rule id>` is that one rule\'s page.\n\n<!-- generated:start rules-model -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `model.schema` | Every key is one its table lists, holding the type its row gives, and every required key is stated. |\n| `model.retired` | `field_roles`, `table_workflows` and `connections` are no longer part of a model: `records` states how a row is recognised, and an app\'s own writes do what a table automation did. |\n| `model.names-declared` | Every alias a key names is declared: an entity of this model, a field of the entity the key reads, an option of the select it names, an act of the app, a template or an app of this model. |\n| `model.tables` | A model declares at least one table and at most 50; apply the rest as a second model. |\n| `model.rows-cap` | Rows are a sample: at most 200 per entity, 2000 per model and 2000 documents attached \u2014 a real data set belongs in an import. |\n| `model.language` | *Noted, never refused:* A label or description written in the other language than the model\'s majority \u2014 with or without diacritics. |\n| `model.alias-unique` | An alias is unique where it is named: entities, roles and templates in the model, fields and views in their entity, options in their select. |\n| `model.label-unique` | A label is unique where apply finds it by label: entities, roles and templates in the model, fields and views in their entity, options in their select \u2014 and no view takes its entity\'s own label, which names the whole-table grid apply makes. |\n| `model.template-sha` | A template\'s `content_sha256`, where stated, is the 64-character lowercase hex sha256 of its content. |\n| `model.template-kind` | An act\'s paper and a register\'s `export` are made from an html or an excel template, never an email one. |\n| `model.filter` | A filter \u2014 a view\'s, a rollup\'s \u2014 tests fields of the entity it reads, through links as `entity.field` hops each standing on the entity the last lands on, by options its select declares. |\n\n<!-- generated:end rules-model -->\n\n## Top level\n\nEvery table of keys on this page is generated from the schema `model apply`\nparses the file with, so it is the whole of what a key may hold.\n\n<!-- generated:start top-level -->\n\n#### Model file\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entities` | list of [Entity](#entity) | yes | The tables this model creates, with their fields, options and views |\n| `roles` | list of [Role](#role) | no | Workspace groups to create; members are added to them afterwards |\n| `templates` | list of [Template](#template) | no | Document templates: html and email inline, excel made from an uploaded workbook |\n| `rows` | map of alias \u2192 list of [Row](#row) | no | First records, keyed by entity alias \u2014 written only where every table they land in is empty |\n| `records` | map of alias \u2192 [Record](#record) | no | How a row of each entity is recognised, keyed by entity alias. Every entity an app lists, opens or picks has one |\n| `write_rules` | map of alias \u2192 [Entity write rules](#entity-write-rules) | no | Entity alias \u2192 what a create of that entity finds, copies and refuses |\n| `apps` | list of ([App](#app) \\| [Dashboard app](#dashboard-app)) | no | The apps this workspace will have \u2014 each one register over an entity, or a dashboard of readings |\n\n<!-- generated:end top-level -->\n\n**A model carries no** `fixtures`, `knowledge` or `knowledge_expects`, and no\n`word` / `pdf-form` template: file content is uploaded to the workspace, never\nstated in a model \u2014 an `excel` template names its uploaded workbook by `file_id`. `apps` here is what an agent states to make an app,\nnever built code. An unknown top-level key is an error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because `apply` matches by label.\n\n## Entity\n\n<!-- generated:start entity -->\n\n#### Entity\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable entity alias, unique within the contract |\n| `label` | text | yes | The table\'s name in the workspace, which apply names it |\n| `singular` | text | no | One row of this table, in the business\'s own words \u2014 what a create\'s button and panel name |\n| `description` | text | no | The table\'s description, written onto the table in the workspace |\n| `writes` | `false` | no | false: no person writes this table\'s rows \u2014 only the writes that keep it do, and no app opens or edits one |\n| `fields` | list of [Field](#field) (at least one) | yes | The table\'s columns |\n| `read_scope` | [Read scope](#read-scope) | no | Which rows a member reads. Absent, every member with access to the table reads every row. |\n| `unique` | list of list of alias (at least one) (at least one) | no | Sets of fields whose values no two live rows share \u2014 each a list of field aliases (text, number, date, a single select, or a link of cardinality "one"). A create or update landing a second row with the same values is refused. |\n| `views` | list of [View](#view) | no | Saved views, in the order they are listed; with none, the table still opens on its default grid |\n\n#### Read scope\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `any` | list of ([Read scope by role](#read-scope-by-role) \\| [Read scope by member](#read-scope-by-member) \\| [Read scope by option](#read-scope-by-option)) (at least one) | yes | A row is readable when ANY of these holds |\n\n#### Read scope by role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `member_of` | alias | yes | A role alias: whoever is in the group it binds to reads the row |\n\n#### Read scope by member\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A select_member field alias on this entity, or on the entity `through` lands on |\n| `is` | `"self"` | yes | The members this column names on a row read that row |\n\n#### Read scope by option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `through` | list of alias (1\u20133) | no | select_record_link aliases from this entity outward, each of cardinality "one" \u2014 the clause\'s field is on the entity the last hop lands on |\n| `field` | alias | yes | A single-select field alias on this entity, or on the entity `through` lands on |\n| `is` | list of alias (at least one) | yes | Its option aliases whose rows are readable \u2014 naming none would hide every row |\n\n<!-- generated:end entity -->\n\n<!-- generated:start rules-entity -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `entity.singular` | An entity a create is mounted over \u2014 a register that adds, a rows block \u2014 states its `singular`: one row of it, in the business\'s words. |\n| `entity.required-cycle` | Required links never wait on each other in a loop: no row of the loop could be created first. |\n| `entity.link-format` | A text field of `format: "link"` holds web addresses; one whose rows hold a phone or a mail address states no format. |\n| `entity.read-scope` | A `read_scope` clause names a declared role (`member_of`) or a field of its entity reached through one-row links: `"self"` a member field, options a single select declares. |\n| `entity.unique` | A `unique` set names two fields or more of its entity once each, each holding one value to compare \u2014 text, a number, a date, a single select, a one-row link \u2014 and is stated once; one field alone is `unique: true` on it. |\n\n<!-- generated:end rules-entity -->\n\n**`unique` is a set of values no two live rows share.** Each entry names fields\nof this entity holding ONE value \u2014 text, number, date, a single select, a link\nof cardinality `"one"` \u2014 and a create or update landing a second row with the\nsame values is refused. A set of one text field is that field\'s own `unique:\ntrue`, so it is refused here. A create carries each set in the names its panel\nsends, so the panel can name the duplicate before the write does.\n\n**`singular` is what a create says** \u2014 `New Order`, `Add Claim line`, `H\u1ED3 s\u01A1\nm\u1EDBi` \u2014 while `label` names the table, so without it the button reads `New\nOrders`. Nothing derives it: English plurals are irregular, and no language is\nexempt. In a language without plural forms it is usually the label itself,\nless any word for the collection. `model apply` REFUSES a model where a table\nsome create opens \u2014 a register\'s own, or a record section\'s add \u2014 states none.\n\n**`writes: false` says only the workspace\'s automations write these rows** \u2014 a\nlog of what the system sent, a copy of what another system holds. No app opens,\nedits or files one: a record listing them keeps the section and opens each row\nat rest, with no Add; a screen over the entity operates at most the rows its\nrecord owns; a party of it is picked, never found or minted; and the generator\nwrites no create and no update for it, and never notes it as created nowhere.\nA `lifecycle` on it is refused \u2014 a row walked\nthrough stages is worked by a person \u2014 and so is a publish desk over it. Absent,\npeople write the rows; `true` is not a value.\n\n**`read_scope` is a ROW rule, enforced by the platform.** It is resolved at\napply into the table\'s own row filters, so an app, a workflow reading for a\nviewer, and the API all answer the same rows \u2014 a per-record visibility field the\napp merely honours is a convention, not a gate. A `"self"` clause reads a column\nof one member or several, and every role alias and option alias a clause names\nmust be one this model declares. `apply` writes the rule onto a table it\nCREATES; a table it adopted that ALREADY CARRIES a rule keeps that one, because\nthe rule is the workspace\'s own statement about its rows \u2014 and a run whose model\nstates a different rule reports the entity rather than leaving the claim silent.\n\n**An app may state that its sharing is its read gate: `"reads": "shared"`.** An\napp reads as its owner, so the rule reaches a viewer only as the predicate every\nquery and editor guard of every app over the entity carries \u2014 right for a desk of\none\'s own rows, wrong for a desk whose audience its sharing already decides, where\nwidening the rule meant a role group nobody remembers to fill. Stated on an APP,\nits queries, pickers and guards carry no entity\'s rule and whoever the app is\nshared with reads and writes every row it draws; every other app and the table\'s\nown filters keep the rule. Share it deliberately. Refused on an app none of whose\ntables states a `read_scope`.\n\n**The rows under a private record INHERIT its rule.** An entity that states no\n`read_scope` and hangs under one that does \u2014 through its `parent` link, over one\nhop or several \u2014 is read by the ancestor\'s rule, answered through that link, on\nits table\'s own filters and in every query and guard alike; nothing is restated,\nso a child needs none of the ancestor\'s columns. Stating a rule on the child\nkeeps that one instead, an ancestor with no rule passes nothing down, and a row\nhanging further under the scoped one than a row filter reaches is refused by\nname \u2014 state a rule on it. So is a hop over a link that names more than one row:\nthe rule would admit a reader any one of them admits while the editor\'s guard\nreads the first, so give the link `"cardinality": "one"` or state a rule on the\nchild.\n\n**ONLY the `parent` role is walked.** A register a scoped record reaches by any\nother link \u2014 the rows that NAME it \u2014 is read by that record\'s id with no rule\ntravelling to it, so it is refused until it states one of its own.\n\n## Field\n\nEvery field carries the keys below, and its `type`\'s section adds the rest; a\ntype whose section names no `default` takes none. `label` may not contain `{` or\n`}` (formulas reference fields by label at the platform level). Every row in `rows` states each\n`required` field it carries (a default is not applied to them), and a required\nLINK is written with its row: the entity it names is created first, and entities\nwhose required links name each other are refused, since none of their rows could\never be created.\n\n<!-- generated:start field -->\n\n#### Field\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"text"` \\| `"number"` \\| `"date"` \\| `"boolean"` \\| `"select"` \\| `"select_member"` \\| `"select_record_link"` \\| `"files"` \\| `"formula"` \\| `"rollup"` \\| `"lookup"` \\| `"autonumber"` | yes | What the field holds \u2014 each type takes the further keys its own section lists |\n| `alias` | alias | yes | Stable local alias, unique within the entity |\n| `label` | text | yes | The field\'s name in the workspace, which apply names it |\n| `description` | text | no | The field\'s description, written onto the field in the workspace |\n| `required` | boolean | no | Refuse a record whose cell for this field is empty. Apply writes it onto the field, and every write path \u2014 create, update, an agent\'s tool call, a workflow\'s set \u2014 refuses the row by field name. |\n\n<!-- generated:end field -->\n\n### `text`\n\n<!-- generated:start field-text -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `unique` | boolean | no | Unique values required |\n| `format` | `"text"` \\| `"link"` \\| `"markdown"` | no | How the words are drawn \u2014 plain, as a link that opens, or as markdown |\n\n<!-- generated:end field-text -->\n\n### `number`\n\n<!-- generated:start field-number -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | number | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n| `format` | `"number"` \\| `"currency"` \\| `"percentage"` | no | What the figure is \u2014 a plain number, money in `currency`, or a percent |\n| `currency` | text | no | ISO 4217 code |\n| `unit` | text | no | What a plain figure counts or measures, drawn after it: a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does. Only beside format "number". |\n| `unit_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number". |\n| `currency_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency". |\n\n<!-- generated:end field-number -->\n\n`format` is what the number IS, and every surface reads it: `currency` prints as\nmoney in the code the row or the field states, `percentage` as a whole percent\nwith its sign. An ABSENT number is drawn absent \u2014 the one exception is a\n`sum` or count rollup the plan reads as a **`measure`**: that is the thing\naccumulated toward a bound, so nothing accumulated yet is zero and the meter\ndraws it. The same rollup read as an `amount` keeps its blank, and so does every\nother role: nothing added to what a row is WORTH means unpriced, not free. A\nformula reading only such sums and counts, and reading zero where each does\n(`{received} - {refunded}`), is one too. A `min`, an `avg`, a percentage of\nnothing and every other formula stay blank in any role, because none of them has\nan answer to give. This is why the pair on one\nscreen reads two ways \u2014 what has come in against what is owed \u2014 and why a\nmeasure\'s own LIMIT, an amount, leaves an unquoted row out of the count rather\nthan reporting it as nothing collected. **AND WHERE THAT LIMIT IS ABSENT \u2014 OR\nZERO \u2014 THERE IS NO LEVEL AT ALL**: a level is a reading AGAINST a bound, so a row\nthat states no bound, or a bound of nothing, draws nothing \u2014 cell, fact and all \u2014\nrather than a numerator whose whole meaning was the comparison. "Collected 0"\nbeside a blank total reads as money against a job worth nothing, and "0 of 0"\nagainst a count of nothing owed claims a comparison nobody can make. A measure the model gives no limit is a plain figure\nand is unaffected. A share is stored in percent units \u2014 68.1 is 68.1 % \u2014 and the\ncolumn, the fact behind it, the meter it is judged by and the figure over the\nregister all say so.\n\n`unit` is what a plain figure counts or measures, and stands only beside\n`format: "number"`. A **measured** unit is a code of one catalog \u2014 mass `g`\n`kg` `t`, volume `l` `m3` `cbm` (a cubic metre under freight\'s name), length\n`mm` `cm` `m` `km`, area `m2`, duration `min` `h` `day` \u2014 so a figure typed in\nanother unit of its dimension converts (`12,5 t` into a `kg` field is 12 500),\nand a tile or a chart reads it in the largest unit it reaches (12 500 kg as\n12,5 t\u1EA5n; CBM never scales) while a cell, a fact and a column always read in the\nfield\'s own. Any other noun of at most 12 characters (`ki\u1EC7n`, `pallet`, `TEU`) is a\n**counted** unit: a word after the figure that never converts. A measured unit is\nwritten as its code \u2014 `t\u1EA5n`, `KG` or `m\xB3` is refused, naming the code. A formula\nstates its own in `formula.unit`; a rollup that keeps the value (`sum`, `avg`,\n`median`, `min`, `max`, `range`) and a lookup carry the unit of the figure they\nread, exactly as they carry a currency, and a count carries none. A quantity is a\nnumber with its unit, never words (`3 cartons` in a text field): only a number\nsums, converts and reads down a column.\n\nA field\'s cells always hold figures in its own unit, so moving it between two\nunits of one dimension (`kg` to `t`) converts every stored figure \u2014 a change made\nin the workspace, in the field\'s settings (which say how many first) or through\n`lotics run update_table`. `model apply` never makes it: the model states the\nworkspace\'s unit until then. Any other change of unit\nrelabels.\n\nA figure whose unit or currency varies by row names a single select of its own\nrow in `unit_field` or `currency_field` (a formula in `formula.unit_field` /\n`formula.currency_field`) \u2014 or a lookup of one through a one-link, a line\nreading its shipment\'s currency. That select\'s options are the vocabulary:\neach label a unit as `unit` takes one (`chi\u1EBFc`, `kg`), or an ISO 4217 code\n(`USD`). A label outside it is refused when the figure is written and when the\nselect is \u2014 its options, its type or its deletion while a figure names it. A row\nwhose select is empty reads its figure bare. Every app reads each row\'s figure in\nits own row\'s unit, and a sum never mixes units: totals are one per unit, a\nchart draws one at a time. A rollup that keeps the value (`sum`, `avg`, `min`\u2026)\nover such a figure stands only where the child\'s select is a lookup, through\nthe rollup\'s own link, of a select on this entity \u2014 the rollup reads that\nselect\'s unit; otherwise it is refused, as is any lookup of such a figure (its\nunit lives on the other row). A count is unaffected. Moving a field between a\nfixed and a per-row unit relabels.\n\n### `date`\n\n<!-- generated:start field-date -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | text | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. A date string in the field\'s format. |\n| `format` | `"date"` \\| `"datetime"` \\| `"date_range"` \\| `"datetime_range"` | no | Whether the field holds a day or a moment, alone or as a span |\n| `timezone` | text | no | IANA timezone |\n| `derive_from` | `"created_at"` \\| `"updated_at"` | no | Auto-populate from the row\'s system timestamp; the field becomes read-only. |\n\n<!-- generated:end field-date -->\n\nA `default` is refused beside `derive_from`: the platform stamps that date.\n\n### `boolean`\n\n<!-- generated:start field-boolean -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | boolean | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. |\n\n<!-- generated:end field-boolean -->\n\n### `select`\n\n<!-- generated:start field-select -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default` | list of alias | no | Value pre-filled into a new record when none is supplied for this field. Applied on create only \u2014 existing records are never backfilled. Option alias(es) this field declares \u2014 one for single-select. |\n| `options` | list of [Select option](#select-option) (at least one) | yes | The choices, in the order every picker and every ladder lists them |\n| `multi` | boolean | no | Allow multiple selections |\n\n#### Select option\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable local alias, unique within the field |\n| `label` | text | yes | Display label for the option |\n| `color` | [colour](#select) | yes | The colour the option\'s badge is drawn in |\n| `mark` | [mark](#select) | no | The option\'s own mark, drawn in place of its colour dot wherever the option is shown: the brand it is ({kind: "brand", name: one of facebook, instagram, threads, meta, tiktok, google-ads, zalo, linkedin, x, google-meet, youtube, telegram, whatsapp, gmail, google-drive, outlook, kiotviet, misa, lark, payos}) or a kit glyph ({kind: "icon", name: "wrench"}). Every option of a field has one, or none does. A mark a reader does not draw falls back to the dot |\n\n<!-- generated:end field-select -->\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\nEvery option is drawn in its colour wherever it shows \u2014 a cell, a row\'s line,\na record, a picker, the bar a reading splits the rows by.\n\nAn option may carry its own `mark`, drawn in place of its colour dot wherever\nthe option is shown \u2014 a stage, a chip, a filter, a fact, an entry of a log, a\nreading\'s part, a lookup of the select on another entity. A select a reader scans\ndown a column \u2014 how a payment was made, the channel, the mode \u2014 states one on every\noption, since a glyph reads before its word: the brand it IS\n(`{"kind": "brand", "name": "tiktok"}` \u2014 one of `facebook`, `instagram`,\n`threads`, `meta`, `tiktok`, `google-ads`, `zalo`, `linkedin`, `x`,\n`google-meet`, `youtube`, `telegram`, `whatsapp`, `gmail`, `google-drive`,\n`outlook`, `kiotviet`, `misa`, `lark`, `payos`), or a glyph the kit draws\n(`{"kind": "icon", "name": "wrench"}`; any other name is refused). Every option of a select has one, or none does: a run of chips\nwhere one carries no mark reads as the one missing something. `apply` writes the\nmarks onto the table, sets one an adopted option lacks, and reports one it wears\ndifferently rather than overwrite it. The glyphs:\n\n<!-- generated:start field-select-glyphs -->\n\nactivity, align-center, align-left, align-right, arrow-down, arrow-down-up, arrow-down-wide-narrow, arrow-left, arrow-left-from-line, arrow-right, arrow-right-from-line, arrow-right-left, arrow-up, arrow-up-down, arrow-up-wide-narrow, ban, banknote, bed, bell, bold, bolt, book-marked, book-open, book-text, bot, box, brackets, brain, briefcase, building-2, calculator, calendar, calendar-clock, calendar-off, camera, car, chart-column, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down-up, chevrons-up-down, circle-alert, circle-check, clipboard-list, clock, code, code-xml, columns-3, columns-3-cog, construction, container, copy, credit-card, database, download, ellipsis, eraser, expand, external-link, eye, eye-off, facebook, file, file-csv, file-down, file-question, file-spreadsheet, file-stack, file-text, file-up, folder, folder-closed, folder-open, folder-pen, form, funnel-plus, funnel-x, gauge, globe, gpu, grip-vertical, group, hand-coins, heading, heading-1, heading-2, heading-3, history, house, image, inbox, info, instagram, italic, keyboard, languages, layout-dashboard, layout-grid, library-big, link-2, link-2-off, linkedin, list, list-checks, list-collapse, list-filter, list-filter-plus, list-ordered, loader, lock, lock-keyhole, lock-keyhole-open, lock-open, log-in, log-out, mail, map-pin, maximize-2, megaphone, menu, message-circle, message-circle-question-mark, message-square, messages-square, mic, minimize-2, minus, monitor, mouse, mouse-pointer-click, music, newspaper, notepad-text-dashed, package, paint-bucket, palette, panel-left, panel-left-close, panel-left-open, panel-right, panel-right-close, panel-right-open, paperclip, pause, pencil, phone, pin, pin-off, plane, play, plug, plus, receipt, rectangle-ellipsis, redo, refresh-cw, repeat, rotate-ccw, rotate-cw, scan, search, send, settings, share, share-2, shield, shield-alert, shield-check, shopping-cart, sliders-horizontal, smile, smile-plus, sparkles, split, square, square-check, square-pen, square-sigma, stethoscope, sticky-note, table, table-2, tag, target, text-quote, thumbs-down, thumbs-up, ticket, trash, trending-down, trending-up, triangle-alert, truck, tv-minimal, twitter, underline, undo, upload, user, user-check, user-pen, users, utensils, waypoints, workflow, wrench, x, zap\n\n<!-- generated:end field-select-glyphs -->\n\n```jsonc\n"options": [\n { "alias": "short_video", "label": "Short video", "color": "zinc", "mark": { "kind": "brand", "name": "tiktok" } },\n { "alias": "print", "label": "Print", "color": "amber", "mark": { "kind": "icon", "name": "newspaper" } }\n]\n```\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet. With no role it is still drawn \u2014\nface and name, ranked as a `party` \u2014 in a screen\'s `columns` and in the register\na record draws of these rows.\n\n<!-- generated:start field-select_member -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `multi` | boolean | no | Allow multiple selections |\n\n<!-- generated:end field-select_member -->\n\n### `select_record_link`\n\n<!-- generated:start field-select_record_link -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `target_entity` | alias | yes | Alias of the entity this field links to |\n| `sync_both_ways` | boolean | no | Create a paired link field on the target entity for bidirectional sync |\n| `paired_field_alias` | alias | no | The pair edge of a bidirectional link: the field alias ON THE TARGET ENTITY that is this link\'s sync partner. Both sides of a pair carry it, each naming the other. Apply creates whichever side it reaches first WITH the pairing (the platform auto-creates the partner) and binds the partner alias to the auto-created field \u2014 without this edge the two contract fields would be created independently and collide with the auto-created partner. |\n| `cardinality` | `"one"` \\| `"many"` | no | How many linked records this field holds. Default \'many\'. \'one\' holds a single row and needs no partner; where the link IS paired, the partner side holds many. |\n| `display_field_aliases` | list of alias | no | Field aliases on the target entity shown as the link\'s display text / picker columns |\n\n<!-- generated:end field-select_record_link -->\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused.\n\n**A single-valued link needs no partner.** `"cardinality": "one"` on its own is a\nlink that holds one row \u2014 one customer on an invoice, one project on a device \u2014\nand nothing is created on the target. The mirror invariant belongs to a PAIRED\nlink: pair a link when the target\'s own record should list what points at it, and\nleave it unpaired when it should not. Either way the record plan draws the\nrelation as a section on the side it points at, so an unpaired link costs the\ntarget nothing.\n\n### `files`\n\nNo keys beyond every field\'s; a row attaches documents to it (\xA7 Rows). Each file\nis drawn by its kind, with no key to choose: a picture (an image, a video) as its\nthumbnail, a document (a PDF, a sheet) as its type\'s badge and its filename.\n\n### `formula`\n\n<!-- generated:start field-formula -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | [Formula](#formula) | yes | Formula config. The expression references other fields on the SAME entity by alias in braces, e.g. `{quantity} * {unit_price}`. |\n\n#### Formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `expression` | text | yes | The expression, over fields of THIS entity by alias in braces \u2014 `{quantity} * {unit_price}` |\n| `format` | `"number"` \\| `"currency"` \\| `"percentage"` \\| `"link"` | no | Display format. \'number\' / \'currency\' / \'percentage\' for numeric results; \'link\' for text-output formulas that return a URL \u2014 renders the result as a clickable link. |\n| `currency` | text | no | ISO 4217 currency code, e.g. USD, VND, EUR |\n| `unit` | text | no | What a plain figure counts or measures, drawn after it: a measured code (g, kg, t, l, m3, cbm, mm, cm, m, km, m2, min, h, day), which converts and scales within its dimension, or any other noun of at most 12 characters (ki\u1EC7n, pallet, TEU), which never does. Only beside format "number". |\n| `unit_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number". |\n| `currency_field` | alias | no | A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency". |\n| `options` | list of [Select option](#select-option) (at least one) | no | The categories the formula yields, drawn as a single select\'s options are (read-only). The expression yields one of them as `{this_field:option}`, or null \u2014 `{days_idle} > 30 ? {warmth:cold} : {warmth:hot}`. Omit for a formula yielding a plain value. |\n| `output_type` | `"number"` \\| `"text"` \\| `"date"` \\| `"datetime"` \\| `"boolean"` \\| `"select"` | no | What the expression YIELDS \u2014 the kind the platform infers at write time, declared here so the offline checks can read it. `format` beside it is how that result is drawn, not what it is. Ignored on the wire (the platform re-infers it); `lotics model pull` writes the inferred value. |\n\n<!-- generated:end field-formula -->\n\n`output_type` is what lets a role or a screen clause accept a computed value: a\ncaption over a derived name (`output_type: "text"`), a period over a settled date\n(`"date"`). A formula declaring neither it nor a `format` says nothing about its\nresult, and every rule that needs one refuses it by name. A formula stating\n`options` is a computed category: read-only, and read as a single select\nwherever it is drawn \u2014 a column, a filter, a reading\'s `by` or `where`.\n\n**A select reaches a formula as the KEYS of its chosen options**, a list \u2014\nnever their labels, and never their aliases \u2014 and a model has no keys: the\nworkspace mints them when the table is made. So an option is named in a formula\nas `{field:option}`, both aliases, and the copy writes that option\'s key in its\nplace: `includes({kind}, {kind:crate})` for a select holding one or several,\n`{kind}[0] == {kind:crate}` for a single one. A select compared to its own words\n(`{kind} != "Crate"`) matches no row and computes the other branch everywhere,\nso the check refuses it and names the token. A select looked up from another\nentity is tested there, in a formula of its own, and that result looked up.\n\n**The language.** The offline check and the platform compute a formula with one\nengine, and the check refuses a call or a name it cannot run, naming what to write:\n\n<!-- generated:start field-formula-language -->\n\n- Operators: `+ - * / %`, `== != > < >= <=`, `&& || !`; `+` also joins text\n- Conditionals: a ternary only \u2014 `{amount} > 100 ? "High" : "Low"`\n- Not supported: optional chaining (`?.`), nullish coalescing (`??`), template literals, arrow functions \u2014 use `get(obj, "path", default)`, `coalesce(v1, v2)`\n- Helpers are these names, spelled exactly; a spreadsheet function (`IF`, `SUM`, `LEN`, `DATEDIF`) is none of them, and a formula calling one is refused\n- Math: round(n,decimals?), ceil(n), floor(n), abs(n), min(a,b), max(a,b), sum(arr), mean(arr), clamp(n,min,max), percentage(part,total,decimals?), pow(base,exp), sqrt(n), mod(n,divisor)\n- Strings: upper(s), lower(s), trim(s), capitalize(s), length(s), contains(s,search), join(arr,sep), split(s,sep), replace(s,search,rep), replaceAll(s,search,rep), startsWith(s,prefix), endsWith(s,suffix), substring(s,start,end?), padStart(s,len,char), padEnd(s,len,char), numberToWords(n)\n- Lists: includes(list,value), first(list), last(list), unique(list), compact(list) \u2014 length(list) counts one\n- Dates: now(), formatDate(d,fmt), addDays(d,n), subDays(d,n), addHours(d,n), subHours(d,n), addMinutes(d,n), subMinutes(d,n), startOfDay(d), endOfDay(d), differenceInCalendarDays(later,earlier), differenceInHours(later,earlier), differenceInMinutes(later,earlier), isBefore(d1,d2), isAfter(d1,d2), isSameDay(d1,d2), isToday(d), isWithinRange(d,start,end), parseDate(d)\n- Null/type: isNull(v), isEmpty(v) (also true for "" and []), coalesce(v1,v2,...), isString(v), isNumber(v), isBoolean(v), isArray(v), toNumber(v), toString(v)\n- Other: formatCurrency(amount,locale,currency), formatDecimal(value,decimals,locale) (grouped quantity, no symbol), get(obj,"path",default?)\n- Empty cells: a cell nobody filled is null inside a formula, whatever its type; one holding 0, false or "0" is not empty. Test it with `isEmpty({note})` \u2014 `{note} == ""` and `{done} == false` are false on an unset cell\n- Arithmetic over an empty cell: `+` and `-` read it as 0 beside a value (`{fee} + {surcharge}` is `{fee}` when the surcharge is empty, null when both are); `*`, `/`, `%` and a unary `-` yield null (`{price} * {qty}` is null, not 0, when the quantity is empty)\n- No helper throws on an empty cell: the math helpers and toNumber return null, the string helpers "". A value of the wrong type still errors. When EVERY field a formula reads is empty it is null \u2014 unless it reads each only as the argument of isEmpty, isNull or isNotNull (`!isEmpty({file})` is false there, not null)\n\n<!-- generated:end field-formula-language -->\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n<!-- generated:start field-rollup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to roll up from |\n| `aggregate_option` | [aggregation](#rollup) | yes | Aggregation operation. Its `field_key` names a field alias on the linked entity. |\n| `filter` | [filter](#views) group | no | Only linked records matching this filter are aggregated; one condition on its own is a group of one. Every `field_key` in it names a field alias on the linked entity, and a select condition\'s value names an option alias there. A traversal node reaches past that entity, so its `path` hops and inner `field_key` are fully-qualified `entity.field` aliases. |\n\n<!-- generated:end field-rollup -->\n\n`aggregate_option` is `{ "operation": \u2026, "field_key": \u2026 }` \u2014 `field_key` a field\nalias on the linked entity (`count` may omit it), and `operation` one of `count`,\n`sum`, `avg`, `median`, `min`, `max`, `range`, `empty`, `filled`,\n`percent_empty`, `percent_filled`, `unique`, `percent_unique`, `earliest`,\n`latest`, `date_range`, `checked`, `unchecked`, `percent_checked`,\n`percent_unchecked`. The operation must be one the aggregated field\'s type\nallows \u2014 `sum` over a number, `earliest` over a date, `filled` over any stored or\nformula field. A lookup is never rolled up: roll up the child\'s own field, or a formula\nover it.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n<!-- generated:start field-lookup -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `source_field_alias` | alias | yes | Alias of a select_record_link field on this entity to look up through |\n| `lookup_field_alias` | alias | yes | Alias of the field on the linked entity to display |\n| `order_by` | [Lookup order](#lookup-order) | no | Show ONE linked row\'s value \u2014 the first in this order \u2014 rather than every linked row\'s |\n\n#### Lookup order\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_key` | text | yes | A field alias on the linked entity the rows are ordered by |\n| `direction` | `"asc"` \\| `"desc"` | yes | Which end of that order the one row is taken from |\n\n<!-- generated:end field-lookup -->\n\nInside a formula, a lookup holding one value is that value (`{due_soon}` is\n`true`, not `[true]`); several values are a list.\n\n### `autonumber`\n\n<!-- generated:start field-autonumber -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `prefix` | text | no | Literal prefix prepended to every display value (e.g. \'KH-\' \u2192 \'KH-001\'). Ignored when `template` is set. |\n| `padding` | integer | no | Zero-pad the integer to this width. Default 1 (no padding). 3 \u2192 \'001\', \'012\', \'123\', \'1234\' (overflow uses the actual width). Ignored when `template` is set. |\n| `template` | text | no | Format template with placeholder tokens evaluated at insert time. Tokens: {N} (raw integer), {N:W} (zero-padded to width W, e.g. {N:3} \u2192 001), {YEAR} (4-digit year), {YEAR:2} (2-digit year), {MONTH} (2-digit month), {DAY} (2-digit day). Date tokens use the workspace timezone. Example: \'HM-{YEAR}-{N:3}\' yields \'HM-2026-001\'. Stored as the composed string; subsequent template edits do NOT re-format existing rows (date tokens would lose the original creation date). |\n\n<!-- generated:end field-autonumber -->\n\n<!-- generated:start rules-field -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `field.default` | A default names options its select declares \u2014 one on a single select \u2014 and a date stamped by `derive_from` states none. |\n| `field.option-mark` | An option\'s `mark` is one the kit draws, and a select\'s options mark every one or none. |\n| `field.unit-select` | A `unit_field` or `currency_field` is a single select of the same row, or a lookup of one through a one-link, whose every option label is a unit, or an ISO 4217 code. |\n| `field.link` | A link targets a declared entity: its `display_field_alias` a field of it, its `paired_field_alias` a link on it naming this one back \u2014 and of two paired links at most one reads one row. |\n| `field.formula` | A formula parses, reads fields of its own entity by alias, names an option as `{field:option}` of one a select declares, and compares a select to its options\' keys, never their words. |\n| `field.rollup` | A rollup aggregates, through a link of its entity, a field of the linked entity by an operation a model declares and that field\'s type takes \u2014 never a figure read in each row\'s own unit or currency \u2014 filtered on fields of the linked entity. |\n| `field.lookup` | A lookup reads, through a link of its entity, a field of the linked entity \u2014 never one read in each row\'s own unit or currency \u2014 ordered by a field of it. |\n| `field.computed-cycle` | Computed fields never wait on each other in a loop: each is computed after what it reads. |\n\n<!-- generated:end rules-field -->\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n<!-- generated:start views -->\n\n#### View\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable view alias, unique within the entity |\n| `label` | text | yes | Display name of the view |\n| `description` | text | no | The view\'s description, written onto the view in the workspace |\n| `columns` | list of [View column](#view-column) (at least one) | no | The columns the view shows, in this order and no others; absent, every field |\n| `filters` | [filter](#views) | no | The rows the view keeps |\n| `sort` | [sort](#views) | no | The order the view reads its rows in |\n| `summary` | map of text \u2192 text | no | Field alias \u2192 the operation its footer cell states |\n| `frozen_columns` | integer \\| `null` | no | How many leading columns stay in place while the rest scroll |\n\n#### View column\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field_alias` | alias | yes | A field of this entity |\n| `visibility` | `"visible"` \\| `"hidden"` | yes | Field visibility state: \'visible\' = shown to everyone, \'hidden\' = not shown by default but members can toggle |\n| `width` | number | no | The column\'s width, in pixels |\n\n<!-- generated:end views -->\n\nA filter is a group \u2014 `{ "node_type": "group", "logic": "and" | "or",\n"children": [ \u2026 ] }` \u2014 or one condition on its own, `{ "node_type":\n"condition", "type": "select", "field_key": "tier", "operator": "has_any_of",\n"value": ["gold"] }`. A sort is a list of `{ "field_key": \u2026, "order": "asc" |\n"desc" | null }`. A condition\'s `type` is the field\'s type and its `operator` is\none that type admits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n<!-- generated:start rules-view -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `view.fields` | A view\'s columns, summary, sort and filters name fields of its entity. |\n\n<!-- generated:end rules-view -->\n\n## Roles\n\nA role becomes a workspace group.\n\n<!-- generated:start roles -->\n\n#### Role\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | Stable role alias, bound to a workspace group when the model is applied |\n| `label` | text | yes | The group\'s name in the workspace |\n\n<!-- generated:end roles -->\n\n## Templates\n\nAn `html` or `email` template carries its content inline, and `{{name}}` in it is filled\nfrom the workflow\'s data; an `excel` template names an uploaded workbook by its `file_id`,\nfilled with the register\'s report by its `export` alone.\n\n<!-- generated:start templates -->\n\n#### Template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `type` | `"html"` \\| `"email"` \\| `"excel"` | yes | html is a page a workflow renders to a PDF; email is a message a workflow sends; excel is a workbook a register\'s export fills |\n| `alias` | alias | yes | Stable template alias, unique within the contract |\n| `label` | text | yes | The template\'s name in the workspace |\n\n<!-- generated:end templates -->\n\n### `html` and `email`\n\n<!-- generated:start template-inline -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `content` | text | yes | Inline template content; placeholders reference field aliases |\n| `content_sha256` | text | no | sha256 (64-char lowercase hex) of the utf-8 content; derived where the template is written when absent |\n\n<!-- generated:end template-inline -->\n\n### `excel`\n\n<!-- generated:start template-excel -->\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `file_id` | text | yes | The uploaded .xlsx (`fil_\u2026`) the template is made from; its markers read what the export fills it with |\n\n<!-- generated:end template-excel -->\n\nA paper that has to look like a counterparty produced it \u2014 an official letter,\nan acceptance minute, a supplier\'s bill \u2014 is the same `html` template with a\nshell around the body: a letterhead, a reference line, a seal and a signature\nblock, and paper grain over everything. One shell, many bodies; the data is the\nonly thing that changes, so a workflow can re-issue it over any record.\n\n```jsonc\n{ "alias": "cong_van", "label": "C\xF4ng v\u0103n", "type": "html",\n "content": "\u2026the page below, as one JSON string\u2026" }\n```\n\n```html\n<style>\n .sheet{position:relative;width:718px;padding:44px 58px 30px;background:#fbfaf6;color:#111;font:14.2px/1.5 \'Liberation Serif\',serif}\n .grain{position:absolute;inset:0;opacity:.34;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'140\' height=\'140\'><filter id=\'f\'><feTurbulence baseFrequency=\'.9\' numOctaves=\'2\'/><feColorMatrix values=\'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .35 0\'/></filter><rect width=\'140\' height=\'140\' filter=\'url(%23f)\'/></svg>")}\n .top{display:flex;text-align:center;font-size:13.4px} .top>div{flex:1} .u{display:inline-block;border-bottom:1px solid #111;font-weight:700}\n .ref{display:flex;text-align:center;font-size:13.4px;margin-top:6px} .ref>div{flex:1} .ref .r{font-style:italic}\n h1{text-align:center;font-size:15.6px;margin:26px 0 18px} p{text-align:justify;text-indent:26px;margin:0 0 9px}\n .sig{display:flex;margin-top:20px} .sig .l{flex:1} .sig .r{width:290px;text-align:center;position:relative}\n .sig .nm{font-weight:700;margin-top:96px} .seal{position:absolute;left:4px;top:8px;width:166px;height:166px;opacity:.66;mix-blend-mode:multiply;transform:rotate(-17deg)}\n </style>\n <div class=\'sheet\'><div class=\'grain\'></div>\n <div class=\'top\'><div><b>{{issuer_parent}}</b><br><span class=\'u\'>{{issuer}}</span></div>\n <div><b>C\u1ED8NG H\xD2A X\xC3 H\u1ED8I CH\u1EE6 NGH\u0128A VI\u1EC6T NAM</b><br><span class=\'u\'>\u0110\u1ED9c l\u1EADp - T\u1EF1 do - H\u1EA1nh ph\xFAc</span></div></div>\n <div class=\'ref\'><div>S\u1ED1: {{number}}</div><div class=\'r\'>{{place}}, ng\xE0y {{day}} th\xE1ng {{month}} n\u0103m {{year}}</div></div>\n <h1>{{title}}</h1>\n <p>K\xEDnh g\u1EEDi: {{recipient}}.</p>\n {{{body}}}\n <div class=\'sig\'><div class=\'l\'><b>N\u01A1i nh\u1EADn:</b><br>- Nh\u01B0 tr\xEAn;<br>- L\u01B0u VT.</div>\n <div class=\'r\'><img class=\'seal\' src=\'{{seal_url}}\'><b>{{signer_title}}</b><div class=\'nm\'>{{signer}}</div></div></div>\n </div>\n```\n\n**What an act\'s template is handed** is its record\'s own fields by alias: a\nselect, a member and a link as their words (a link as the linked row\'s title), a\nnumber, a date and a computed value that declares its kind raw (`2800000`,\n`2026-09-14`) \u2014 never a files field or a computed select. On one record it is\nhanded, too, the rows of each child the record draws as `rows` whose alias the\ntemplate names, listed under that alias for `{{#each <alias>}}` in place of the\nrecord\'s own link to them: every row filed under the record, oldest first, each\nby its own fields as the record\'s are, less its link back to the record. Over\nseveral rows the rows are listed under the entity\'s alias, for\n`{{#each <alias>}}`, without their child rows.\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model, attaching at most 2000 documents between them \u2014 a real data set\nbelongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n<!-- generated:start rows -->\n\n#### Row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `ref` | text | yes | Local handle for this row, referenced by other rows\' link fields |\n| `fields` | map of text \u2192 any value | yes | Field alias \u2192 the value, read against the field\'s declared type |\n\n<!-- generated:end rows -->\n\n<!-- generated:start rules-rows -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `rows.ref` | A row\'s `ref` is unique within its entity. |\n| `rows.required` | A row states every field its entity requires: it is created with what it states, its fields\' defaults unapplied. |\n| `rows.distinct` | Open rows of an entity a register lists read apart: no two state the same title and the same line under it. |\n| `rows.value` | A row\'s value fits its field: an option alias for a select, `"<entity>:<ref>"` of a row of the linked entity in this file for a link, `"self"` for a member, a relative path beside the file or a `fil_` id for files, a date in the date grammar (an hour only on a datetime), a text, number, yes/no or null otherwise \u2014 and none on a computed field. |\n\n<!-- generated:end rules-rows -->\n\nA `ref` is lowercase letters, digits and underscores, and is never persisted.\n\nA `files` cell attaches documents: paths relative to this file (no `..`, never\nabsolute), which the check proves exist and `apply` uploads into the workspace\nbefore any row is written \u2014 a paperwork business seeds its papers with its\nrows. The server accepts only `fil_` ids of files this workspace owns, which is\nwhat the upload leaves behind. After a run that wrote rows, the WORKSPACE holds\nwhich record each row became, under the row\'s own `<entity>:<ref>`:\n`delete_records` over them is how a seeded set is reset, and applying again\nre-dates it.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several; a paired link is stated on ONE side (the child\'s link to its parent) and its partner fills itself |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | paths beside this file \u2014 `["scans/pccc_letter.png"]` \u2014 uploaded by `model apply`/`setup` before the model is sent; or `fil_` ids of files already in this workspace |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n- on a date that holds its hour (`format: "datetime"`), that hour after the day, and always stated:\n `@today 14:30`, `@today+1 06:00`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Records\n\n`records` says how a row of each entity is RECOGNISED \u2014 once per entity, keyed by\nentity alias \u2014 and every surface that draws one of its rows reads the same\nstatement: the register\'s row, a picker\'s option, a link\'s chip, a child table\'s\nrow and the record\'s header. Every entity an app lists, opens or picks has one.\n\n```jsonc\n"records": {\n "visit": {\n "title": "container_no",\n "subtitle": ["customer", "arrived_on"],\n "image": "photos",\n "status": { "field": "stage", "closed": ["gone"], "history": "visit_history" },\n "figure": "total_fees",\n "due": ["free_until"]\n },\n // A release order read against what it allows: a meter wherever it shows.\n "release": { "title": "release_no", "figure": "issued", "limits": { "issued": "units" } },\n // A dossier\'s stage is the last date it reached \u2014 no stored select.\n "dossier": { "title": "applicant", "status": { "milestones": ["received_on", { "field": "appraised_on", "when": { "kind": ["loan"] } }, "signed_on"], "closed": ["signed_on"] } }\n}\n```\n\n<!-- generated:start records -->\n\n#### Record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | alias | yes | The field that names a row \u2014 the first thing every surface shows: who or what it is, a link naming it by the linked row (a job read by its customer). An autonumber is refused; a typed number is the subtitle |\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title wherever a row is drawn (a code, a date, a party) |\n| `image` | alias | no | A files field that pictures the row \u2014 never one an act keeps the document it makes in |\n| `description` | alias | no | A text field saying what the row is about \u2014 a plan\'s brief, a customer\'s note: read in its record\'s head under the title\'s line, its first lines at rest and whole on a press, and written from the head\'s \u270E. Never on a row\'s line |\n| `party` | `"person"` \\| `"organization"` | no | What each row is where the rows are people (a contact, a patient) or organisations (a customer, a supplier, a carrier); absent, the rows are things (an order, an item, a paper) |\n| `status` | [Status](#status) | no | The stage a row moves through \u2014 a single select or its milestone dates \u2014 drawn as a badge beside its title |\n| `figure` | alias | no | The one number the row stands for, read at its right (a total, a quantity left) |\n| `limits` | map of alias \u2192 (alias \\| number) | no | A number read against a bound \u2014 a field of the same row or a constant \u2014 drawn as a meter wherever it shows |\n| `gates` | map of alias \u2192 (alias \\| number) | no | A pass mark on a meter `limits` bounds \u2014 a field of the same row or a constant; a percent field is its share of the bound, any other number an amount. The meter marks it and fills complete once past it |\n| `tolerance` | map of alias \u2192 number | no | How far past its `limits` bound a meter may stand, in percent of the bound: the bound is an amount expected (received against ordered), not a cap. Without one the bound is a cap, and past it is an overrun |\n| `due` | list of alias (at least one) | no | Dates that are deadlines (stored, or computed as a date): each counts down and turns overdue |\n| `frees` | alias | no | A date ending the row\'s span of days on which what the row holds is free again (a check-out, a hire\'s return): a stay from the 3rd to the 5th holds the 3rd and the 4th, and another may start on the 5th \u2014 on a lanes board, a calendar, a roster and in `no_overlap` alike. Absent, a span of days holds its last day; a span of moments always frees its end |\n| `task` | `true` | no | A row is work someone finishes: wherever rows of it stand in a table \u2014 a register, a record\'s rows, the rows filed under one \u2014 each is led by a ring ticking it done and unticking it by the one write of the app making that move. Its status is a stored select with `closed` |\n| `applies` | map of alias \u2192 map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Fields that apply only while their conditions hold (a length only on a line whose tariff charges by the metre): where one does not apply no add asks it and no surface shows it. Never the title; a required one starts at its `default` |\n| `starts` | map of alias \u2192 `"today"` \\| `"me"` | no | What a field of a row being added starts at, where that start is certain: `today` a date (a moment starts now), `me` a member, the reader adding it; every other field starts empty unless the reader narrowed the register to a value or the field declares a `default` |\n\n#### Status\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | no | The single select that is the row\'s state \u2014 or `milestones` instead |\n| `milestones` | list of (alias \\| [Milestone](#milestone)) (at least 2) | no | Dates in the order the work reaches them, instead of a stored select: the stage is the last one filled, ticking one stamps today, and clearing one clears every later one |\n| `closed` | list of alias (at least one) | no | Options (or milestones) that end the work: a register opens on the rows not at them, and the badge reads muted |\n| `history` | alias | no | A child entity every status change appends one row to (its link to this entity, the new status, the moment and who) \u2014 written by the app\'s own writes, never a table automation; a row the model seeds opens it at its status, the day the rows land, unless the model states its rows; with `field` only |\n\n#### Milestone\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A date field: the stage is reached on the day it holds |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | yes | The stage applies only while each named select holds one of these options, and each named stored yes/no is this value; otherwise the row skips it |\n\n<!-- generated:end records -->\n\n<!-- generated:start rules-records -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `records.title` | A title names the row by what it is \u2014 never an autonumber; a code is its subtitle. |\n| `records.image` | `image` is a files field, never one an act keeps the documents it makes in (`into`). |\n| `records.description` | `description` is a text field, read in the record\'s head \u2014 never a section\'s too. |\n| `records.status` | A status is one single select, `closed` naming options of it \u2014 or its `milestones`. |\n| `records.history` | A declared status `history` has exactly one link to the row it records, one single select holding every option of the status (or a text), exactly one date and at most one member. |\n| `records.history-derived` | A `history` the model does not declare is derived and keeps one entity\'s moves, labelled by that entity ("L\u1ECBch s\u1EED <entity label>", or "<entity label> history"): no declared table holds that label, and the entity has no other field under the link it adds. |\n| `records.history-scope` | A status history reads by its record\'s `read_scope`, answered through its one-row link to the record within the links a row rule reaches \u2014 or it states its own. |\n| `records.milestones` | Milestones are dates a person ticks, each named once; `closed` names some of them, and a deadline (`due`) is never one. |\n| `records.milestone-add` | An add asks the first milestone at most \u2014 each after it is ticked in order. |\n| `records.figure` | A figure is a number. |\n| `records.applies` | `applies` holds a field of the row other than its title to conditions of the same row \u2014 a yes/no (stored or a formula) by `true` or `false`, a single select by its options, its own or looked up through a one-row link \u2014 never to itself; a required field it holds starts at a `default`. |\n| `records.limits` | `limits` bounds a number by a number field of the row, in its unit or one of the same dimension \u2014 or by a constant, where every row reads the figure in one unit. |\n| `records.gates` | `gates` marks a pass on a meter `limits` bounds: a percent field (its share of the bound), another number in the bound\'s unit, or a constant. |\n| `records.tolerance` | `tolerance` stands on a meter `limits` bounds. |\n| `records.starts` | `starts` starts a field a person writes \u2014 `today` a date or a moment (never a span), `me` a member \u2014 and an add that does not ask a field it starts writes that start. |\n| `records.due` | A deadline (`due`) is a date. |\n| `records.frees` | `frees` names a stored day, never a moment: a span of moments already frees its end. |\n| `records.task` | A task entity (`task`) states a `status` of a stored single select with `closed` options \u2014 its ring ticks a row done by moving it to one. |\n\n<!-- generated:end rules-records -->\n\n- **The title names the row** by who or what it is \u2014 a link by the linked row (a job\n read by its customer). An autonumber is refused as a title; a typed number is the\n subtitle.\n- **The status** is one single select. `closed` options end the work: a register opens\n on the rows not in them. A `history` entity gets one row per move \u2014 its link to this\n entity, the new status (a select holding the same option aliases, or text), the\n moment and who \u2014 appended by the app\'s own writes that move the status, never by a\n table automation. A `history` the model does not declare is derived, and needs no\n `records` entry of its own.\n- **Milestones** are the alternative to a stored select: dates in the order the work\n reaches them, the stage being the last one filled (never stored). A milestone with a\n `when` applies only while its select holds those options and its stored yes/no that\n value; otherwise the row skips it.\n The record draws them as one block where ticking stamps today and clearing one clears\n every later one; every save and act re-checks that a date comes after each earlier one\n that applies. `closed` names the milestones that end the work. A register\'s add may ask\n the first milestone, never a later one; an act may stamp one, never clear it.\n- **A limit** reads a number against a bound \u2014 another field of the row, or a constant.\n A `gates` entry marks a pass on that meter: a percent field is its share of the\n bound, any other number an amount.\n- **A deadline** (`due`) counts down wherever it is drawn and is overdue once past while\n the work is open. A register over the entity says how many rows in view are overdue on\n its summary line, and pressing that count narrows to them \u2014 no reading to state.\n- **A span\'s free day** (`frees`) is the end date on which what the row held is free\n again \u2014 a check-out, a hire\'s return. A stay from the 3rd to the 5th holds two nights:\n a lanes board, a calendar and a roster draw it through the 4th, and `no_overlap` lets\n the next stay start on the 5th. Without it a span of days holds its last day; a span of\n moments always ends as the next may begin.\n- **`starts`** is where an add\'s field begins, stated only where that is certain \u2014 a\n log\'s own day at `today`, a request\'s requester at `me`. Nothing else starts a field\n but its declared `default` and what the reader narrowed the register to.\n\n## Write rules\n\n`write_rules` is what a WRITE meets, keyed by entity alias; each entity\'s is held\nby its table. Every generated write of an\napp (`create_<entity>`, `update_<entity>`, each act) re-checks these on the\nserver; the screen only mirrors them.\n\n```jsonc\n"write_rules": {\n // A customer is RECOGNISED by their address. An add that names one \u2014 from the\n // customers register or from a picker\'s "new" \u2014 reuses the row it matches and\n // opens one only where nothing does, so the book never grows a second Acme.\n "customer": { "natural_key": ["email"] },\n "order_line": {\n "fields": {\n // A line of nothing is not a line. Refused on create and on update.\n "quantity": { "min": 1, "max": 9999 },\n // Shipped inside its order\'s window: a bound read off the row a link names\n // is held from both sides \u2014 moving the order\'s dates never strands a line.\n "ships_on": { "min": "order.placed_on", "max": "order.due_on" },\n // The price is fixed at the moment of ordering \u2014 COPIED off the product,\n // not looked up for ever after.\n "unit_price": { "default_from": "product.price" },\n // Nothing is sold off an empty shelf. The picker reads only the rows that\n // answer this, and the write refuses the same rows again.\n "product": {\n "options_where": {\n "node_type": "group", "logic": "and",\n "children": [{ "node_type": "condition", "type": "number",\n "field_key": "in_stock", "operator": "greater_than", "value": 0 }]\n }\n }\n }\n },\n // One booking per room at a time, while it is held: a create or a save whose\n // span overlaps another held booking of the same room is refused.\n "booking": {\n "fields": { "ends_on": { "min": "starts_on" } },\n "no_overlap": { "from": "starts_on", "to": "ends_on", "by": ["room"], "while": { "state": ["held"] } }\n }\n}\n```\n\n<!-- generated:start write-rules -->\n\n#### Entity write rules\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `natural_key` | list of alias (at least one) | no | The field aliases a row of this entity is RECOGNISED by. A write that names a row of this entity by these values reuses the row it finds, minting one only where nothing matches. |\n| `fields` | map of alias \u2192 [Field write rule](#field-write-rule) | no | Field alias \u2192 what that field\'s value is copied from, bounded by or picked among |\n| `no_overlap` | [No overlap](#no-overlap) | no | No two rows hold overlapping spans \u2014 a create or update that would is refused |\n\n#### Field write rule\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `default_from` | text | no | Copy this field\'s value from the linked row at CREATE time \u2014 `<link alias>.<field alias>`, the link being a one-row link on this entity, required or filled by the add (the record a row is added under). The value is copied rather than looked up, so the source changing later leaves the row alone. |\n| `min` | number \\| text | no | Refuse a create or update whose value is below this: a constant for a number, or a field \u2014 `<field alias>` of the same row, or `<link alias>.<field alias>` of the one row a link names \u2014 of the same type (a return date after the departure, a line\'s day inside its trip). On a figure whose unit or currency is per row (`unit_field`, `currency_field`) a constant bound is only 0; bound it by a field of the row |\n| `max` | number \\| text | no | Refuse a create or update whose value is above this: a constant for a number, or a field of the same row or of the one row a link names |\n| `options_where` | [filter](#views) | no | Which rows of the target this link may point at \u2014 an `and` group of plain conditions over the TARGET entity\'s own fields, each `field_key` a field alias. It narrows the picker\'s read and is refused again where the write lands. |\n| `same` | list of alias (at least one) | no | One-row links this row and each row this link points at must name alike \u2014 a fee\'s invoice is one of its own visit\'s, a box\'s seal one of its own shipping line\'s: each a one-row link of both entities to the same entity. It narrows the picker\'s read to the rows naming what this row names, and a write is refused wherever it lands \u2014 on the link, or on what the two rows compare. |\n| `suggest` | text | no | A text field whose control offers, as the reader types, the distinct values a text field of a catalog holds \u2014 `<entity alias>.<field alias>` (a damage position\'s code among the codes on file); any text is still written |\n\n#### No overlap\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The stored date (or datetime) a row\'s span starts on |\n| `to` | alias | yes | The stored date (or datetime) a row\'s span ends on \u2014 a day it holds, unless `records` names it the day the row `frees`; a moment another row may start at |\n| `by` | list of alias (at least one) | no | Fields two rows share to compete for a span (the same employee, the same room); absent, every row |\n| `while` | map of alias \u2192 list of alias (at least one) | no | Only rows at these options of these selects hold their span (running, not closed); absent, every row |\n\n<!-- generated:end write-rules -->\n\n<!-- generated:start rules-write -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `write.natural-key` | A `natural_key` is text or a number a person types back, or a one-row link; a text key is unique \u2014 `unique: true`, or the key\'s fields as a set in the entity\'s `unique`. |\n| `write.default-from` | `default_from` copies `<link>.<field>` off the row a one-row link names, wherever an add fills that link (asked, or the record it is added under), into a field of the same type: a select into one declaring each of its options by alias, several options only into a multi-select. |\n| `write.bounds` | `min` and `max` bound a number or a date \u2014 by a constant a number, by a field of this row or of the one row a one-row link names either, of the same type and unit, never by itself \u2014 and `min` is never above `max`. |\n| `write.bound-per-row-unit` | On a figure whose unit or currency is the row\'s own (`unit_field`, `currency_field`) a constant bound is only 0 \u2014 bound it by a field of the row. |\n| `write.suggest` | `suggest` stands on a stored text field and names `<entity>.<field>`, a stored text field of a declared entity; the app reads that entity, so it has a `records` entry. |\n| `write.same` | `same` stands on a link and names one-row links this entity and the link\'s target both hold to one entity. |\n| `write.options-where` | `options_where` narrows a link by an `and` group of plain conditions over the target\'s own fields \u2014 a number compared in one unit on every row, a select by options it declares. |\n| `write.no-overlap` | `no_overlap` spans two date fields; `by` names values rows share \u2014 a link, a person, one option, a text or a number; `while` names single selects by their options. |\n\n<!-- generated:end rules-write -->\n\n- A `natural_key` is `text` or `number`, because a person types it back, or a\n one-row link, because a row is also recognised by the one row it belongs with (a\n fee by its visit and its kind). A key holding a `text` field is unique: `unique:\n true` on that field, or the key\'s fields as a set in the entity\'s `unique` (a\n site by its customer and its name) \u2014 two rows sharing it would make\n find-or-create pick whichever the read answered first. An add asks the key by\n default.\n- A `min` or `max` naming a field is read off the row as the save would leave it,\n and one naming `<link>.<field>` off the row the link names \u2014 the link\'s side\n re-checks its own rows when that bound moves.\n- A `default_from` copies where the add is sure to hold its link\'s row \u2014 a\n required link, or the record the row is added under (an appointment added under a\n plan takes the plan\'s patient) \u2014 and the two field types match. The copied field is\n not asked there; through an optional link the add leaves empty, it is asked.\n- A `same` link names only rows that name what this row names \u2014 a fee\'s invoice\n is one of its own visit\'s \u2014 through a one-row link both entities hold to one\n entity; its picker offers nothing until the row names one. A row added under a\n record names what it copies off it (`default_from`) from the start, so a block\n expecting one line per row of the link lists that record\'s rows alone. A save\n moving what the two compare, on either row, is refused while they would name apart.\n- An `options_where` link may be left empty; a row it names is refused again\n where the write lands unless it holds the narrowing. A link naming one kind of\n a book\'s rows (a "Shipping line" among the parties) narrows to that kind, or\n its picker offers every row.\n\nA field\'s own `required` is not here: the contract carries it, and every write\npath refuses the row by field name from it. `unique` is the text field\'s own\nclause (\xA7 `text`) \u2014 an add says so at the control before the column does. A\nfield the workspace writes (a formula, a rollup, a lookup, an autonumber, a date\nwith `derive_from`) is never asked, written or edited.\n\n## Apps\n\nAn app is ONE register over one entity and the record each row opens \u2014 or a dashboard\nof readings. The author states COMPOSITION \u2014 which fields go where; the runtime owns\nhow each thing looks, one treatment per concept. Nothing is drawn that the app does not\nname, and each fact appears once on a record. What the vocabulary has no word for is an\nact\'s own `workflow`.\n\n```jsonc\n"apps": [{\n "alias": "gate", "name": "Gate in and out", "entity": "visit",\n "register": { "columns": ["service"], "filters": ["customer", "service"] },\n "record": {\n "sections": [\n { "title": "In", "fields": ["customer", "service"], "blocks": [{ "rows": "fee", "columns": ["amount"] }] },\n { "title": "Out", "at": ["in_yard"], "fields": ["release", "seal"], "blocks": [{ "files": ["photos"] }], "acts": ["gate_out"] }\n ]\n },\n "acts": [{ "alias": "gate_out", "label": "Gate out", "when": { "stage": ["in_yard"] },\n "requires": ["release", "seal"], "set": { "stage": "gone", "left_on": "now" }, "confirm": true }],\n "checks": [{ "field": "unpaid", "blocks": ["gate_out"] }]\n}]\n```\n\n<!-- generated:start apps -->\n\n#### App\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `entity` | alias | yes | The entity whose rows this job works \u2014 the register\'s rows |\n| `books` | list of [Book](#book) (at least one) | no | Other entities the register reads as one list with `entity`\'s rows, each opened, edited and acted on in its own table: the same app over each, its fields lined up with `entity`\'s. Only a table or cards |\n| `scope` | alias | no | A required one-row link of the entity: the app works inside one row of the linked entity at a time (a project, a branch), picked from a list of them, and every row it reads, counts and adds is that row\'s |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `writes` | `"children"` | no | The reader adds and edits the record\'s child rows but not the record\'s own fields; the app\'s acts still move it |\n| `row` | [App row](#app-row) | no | How this job reads the entity\'s rows where it reads them otherwise than `records` (a cashier\'s figure is what is owed): the subtitle, figure and image stated replace the entity\'s wherever this app draws its rows \u2014 the register, a card, the record\'s header, a calendar, lane or roster entry. The title stays the entity\'s |\n| `register` | [Register](#register) | no | The rows: which columns and filters, the order, how a row is added |\n| `record` | [Record page](#record-page) | no | What a row opens: the door, its sections, and beside them its comment thread and its status history where stated |\n| `acts` | list of [Act](#act) | no | What the reader does to a record \u2014 each a press with its conditions and its write |\n| `checks` | list of [Check](#check) | no | Formulas that warn while they stand, or refuse the acts and saves they name |\n\n#### Theme\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `color` | text | yes | The tile\'s colour |\n\n#### App row\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `subtitle` | list of alias (1\u20132) | no | Up to two fields read under the title in this app, in place of the entity\'s |\n| `figure` | alias | no | The one number a row stands for in this app, in place of the entity\'s; a `limits` bound on it reads as its meter |\n| `image` | alias | no | A files field that pictures a row in this app, in place of the entity\'s |\n\n#### Book\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `entity` | alias | yes | Another entity whose rows the register reads beside its own \u2014 the same job over another table (a legacy ledger, a second branch\'s book) |\n| `fields` | map of alias \u2192 alias | no | A field of the app\'s entity \u2192 the field of this entity holding the same fact, where their aliases differ; a field the app reads lines up by its own alias otherwise, and one this entity lacks is blank on its rows and drops from what they open \u2014 an act, check or add needing it is not offered on them. Options line up by alias |\n\n#### Dashboard app\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The app\'s name within the model \u2014 what `--from model.json#<alias>` picks |\n| `name` | text | yes | The job, in the words of the people who do it |\n| `description` | text | no | What the job is for, in a sentence |\n| `icon` | text | no | A lucide icon name the launcher tile draws |\n| `theme` | [Theme](#theme) | no | The launcher tile\'s colour |\n| `reads` | `"shared"` | no | Every member reads every row, whatever the entity\'s read scope |\n| `dashboard` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot) \\| [List](#list)) (at least one) | yes | The readings, in order \u2014 each over every row of its entity, windowed by the one period the reader switches |\n\n<!-- generated:end apps -->\n\n<!-- generated:start rules-app -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `app.alias-unique` | An app\'s alias is unique among the model\'s apps, and an act\'s among its app\'s acts. |\n| `app.records-entry` | Every entity an app lists, opens or picks \u2014 its own, a block\'s, a linked row\'s, a document\'s \u2014 has a `records` entry. |\n| `app.reads-shared` | `reads: "shared"` stands only where a table the app reads states a `read_scope`. |\n| `app.writes-children` | An app that writes only the record\'s children (`writes: "children"`) adds no row of its own entity. |\n| `app.scope` | `scope` is a required one-row link of the app\'s entity, the same on every row the app lists \u2014 never a column, filter, search field, group, `where`, `opens`, section field or add question. |\n| `app.when` | A `when` holds single selects to options and yes/nos to `true` or `false`, stored or a formula of one \u2014 a milestone\'s `when` only a stored yes/no, read before any formula over it is computed. |\n| `app.asked-written` | What an add, an act or `starts` fills is a field a person writes \u2014 never a formula, rollup, lookup, autonumber or a date the workspace stamps. |\n| `app.own-rows` | A many-link to rows that each belong to one row of this entity is never a section\'s field, an add\'s question or an act\'s ask: those rows are the record\'s own, read as a `rows` block. |\n| `app.create-asks` | An add asks fields of a table people write \u2014 never the link or the folder it files the row under, which the add sets itself. |\n| `app.create-required` | An add (`create`) asks every field its entity requires that nothing else fills \u2014 a `default`, `starts`, the record or folder it files the row under, or a `default_from` over a link it fills \u2014 or the server refuses every add. |\n| `app.books` | A register app\'s `books` each name another entity once \u2014 never the app\'s own \u2014 whose fields line up with the app\'s by alias, else by `fields` (a field the app reads, or one a report prints \u2192 one of the book\'s): read as one column type (a day apart from a moment), one value or several alike, a link to the same entity. Each book holds the title, the status, the folder (`scope`), what `where` keeps and every check locking the rows; the register is a table or cards. Two books holding a field of their own under one alias hold it as one kind of value. A report\'s `books` each name the app\'s entity or one of its books, once, and one of them holds each field its `where` and `per` narrow by. |\n| `app.book-lacks` | *Noted, never refused:* A book lacking a field the app\'s record, an act or an add reads: what reads it drops from the book\'s rows \u2014 an act, a block, a section, the add. A field only a report prints, held by a book as another kind of value, prints blank on its rows. |\n| `app.row` | An app\'s `row` states only what differs from `records`: a line that is not the title, a number for its figure, a files picture no act makes documents into. |\n| `app.files-shown` | Every files field is drawn by some app over its entity \u2014 a section\'s `fields`, a `files` block, a column, the `image`, or where an act making its papers into it (`into`) stands, a section naming the act: the made paper reads under the act, one per template. |\n| `app.document-image` | *Noted, never refused:* A linked entity holding one files field and no `image`: `image` lets a link preview its paper. |\n| `app.tasks` | *Noted, never refused:* A task entity an app lists whose rows no write of the app moves to a closed option (its ring stands disabled), or that an act ticks done and none moves back. |\n\n<!-- generated:end rules-app -->\n\n`writes: "children"` is a desk that adds and edits a record\'s child rows and never\nthe record itself. `reads: "shared"` lifts every read scope the app\'s tables state \u2014\nrefused where none of them states one.\n\n`scope` names a required one-row link of the entity: the app works inside ONE row of\nthe linked entity at a time \u2014 a project, a branch, a season. With no folder remembered\nit opens on the list of them, each with how many of the app\'s rows it holds; inside,\nevery read, count and reading narrows to that one, an add files its row under it (the\nlink is never asked), and the record states its folder in the header. A scope narrows\nthe view; who may read what stays the entity\'s `read_scope`. The folder link is never a\ncolumn, a filter, a search field, a fact or an add\'s question.\n\nA dashboard is `{ "alias", "name", "dashboard": [readings] }` with no entity, register\nor record: each reading reads every row of its entity, and one period the reader\nswitches (this month \xB7 30 days \xB7 this quarter \xB7 this year) windows each reading placed\nin time; one placed in no time reads the rows as they stand now, and its head says so.\nA `list` reading lists rows, each opening in its `app`. The readings answer the one\nquestion the dashboard\'s owner asks, in the order they ask it \u2014 rows to act on lead where\nthe answer is rows; no order of kinds is the rule.\n\n```jsonc\n// The depot owner\'s morning question: which boxes are past their free days, whose are they, is it growing?\n{ "alias": "overview", "name": "Depot overview", "dashboard": [\n { "list": "visit", "where": { "overdue": true }, "columns": ["line"], "app": "gate", "label": "Past free days" },\n { "breakdown": "visit", "by": "line", "where": { "overdue": true }, "label": "Past free days by line" },\n { "trend": "visit", "over": "arrived_on", "where": { "overdue": true }, "label": "Past free days, by arrival" }\n] }\n```\n\n### Register\n\n```jsonc\n"register": { "columns": ["service", "release"], "filters": ["customer", "service"], "create": ["container_no", "customer"] }\n```\n\n<!-- generated:start apps-register -->\n\n#### Register\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `columns` | list of alias | no | Fields read as columns after the row\'s title \u2014 on a calendar\'s entry or a lane\'s block, its facts after its title; never what the row draws itself (its subtitle, image, status, figure, or the bound its figure\'s meter reads against), which the runtime places |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The fields the reader narrows the rows by every day, in that order \u2014 each earns its place by that daily use, so none to 3 is normal and 3 is a cap, never a quota (a select, a member, a link, a date, a yes/no, a number as a range, or its `tiers` where the business narrows by those bands daily) \u2014 each on a desk\'s line, in one Filters sheet on a phone. Never the status (its chips); each a field the rows show \u2014 a column, or a part of the row. A threshold the business names ("large orders") is a formula yes/no here; a column\'s header sorts by a day or an amount |\n| `search` | list of alias (at least one) | no | The fields the search box matches (a link by its row\'s title); a pasted list searches each line and names the lines no row matched. Absent, every word of the row |\n| `group` | alias | no | The field the rows open grouped by, the reader\'s grouping starting there \u2014 a period\'s rows read under each period, one per person; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order rows open in: one key, or a list of keys most significant first, each breaking the ties of the ones before it (a select orders by its options\' order); absent, the first deadline soonest first, else the newest first |\n| `tabs` | list of [Register tab](#register-tab) (2\u20135) | no | The lists the register reads its rows as, each a tab with its count over one period the reader picks above them \u2014 the stock at its end beside the arrivals and departures during it, a report\'s sheets; each keeps the register\'s filters and search. Absent, one list. Not with a calendar, roster or lanes, which read their own window |\n| `period` | `"today"` \\| `"week"` \\| `"month"` \\| `"quarter"` \\| `"year"` \\| [Period days](#period-days) | no | With `tabs`: the period they open on \u2014 `today`, `week`, `month`, `quarter` or `year` so far, or `{ days }`, the last that many days through now (a night shift running past midnight reads two); absent, this month so far |\n| `layout` | `"table"` \\| `"cards"` \\| `"calendar"` \\| `"roster"` \\| `"lanes"` \\| `"gantt"` | no | `cards` for rows read by their picture; `calendar` for rows read by their day \u2014 a month, a week, a day or a list, an entry at its hour where its date holds one and over its span where its line holds a second date; `roster` for what each row did on each day of a week or a month (who worked which shift, which vehicle ran), or with `expect` which of a set each row holds; `lanes` for rows booked on a resource over time (an appointment in its chair, a hire on its machine); `gantt` for rows each planned over a run of days, one bar a row on one axis of time (a shipment from its sailing to its arrival, a hire from its start to its return, a task of a project) \u2014 its dates in `gantt`, its lanes the register\'s `group`; absent, a table |\n| `gantt` | [Register gantt](#register-gantt) | no | With `layout: "gantt"`: the dates and facts each row\'s bar is drawn from |\n| `roster` | alias | no | With `layout: "roster"`: the entity whose rows fill the days \u2014 one single link to this entity, and a date on its row line (`records` title or subtitle). A cell reads its row\'s status, else the first single select on its line, several rows a mark each, the cell read by the one latest in that select\'s options; a row whose line holds a second date after the first fills every day to it; a day with none stays empty, never an absence |\n| `expect` | alias | no | With `roster`: a single select of the roster\'s rows whose options are the columns in place of days \u2014 each cell the row holding that option, read by its status, an empty one added there (a checklist of papers per case); the roster then has no period |\n| `of` | alias | no | With `expect`: this entity\'s own multi-select holding the options each row expects \u2014 its other columns stand blank on that row, never added. Its options are among `expect`\'s |\n| `lanes` | alias | no | With `layout: "lanes"`: a one-link of this entity whose target\'s rows are the lanes (a chair, a machine, a room), each drawn even when empty. A row is a block from the first date on its line to the second \u2014 within one day by the hour where the first holds one, else by the day over one day, a week or a month; a free stretch between blocks adds a row there, its lane, its start and the end the stretch reaches filled |\n| `loads` | map of alias \u2192 alias | no | With `lanes`: what a lane carries against what it holds, at most 2 \u2014 a number of this entity each block adds (a weight) to a number of the lane\'s row it is held within (a payload), in one unit or two of one dimension. Each lane reads the most its blocks add up to at one time in the window \u2014 a day\'s rows together \u2014 and names by how much and when a lane runs over |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Only the rows whose select holds one of these options and whose yes/no (stored, or a formula) is this value \u2014 read so on the server and never offered as a chip: the rows this app works of an entity other apps read whole (the purchases, of orders both ways). A row added here holds a select\'s one option and a stored yes/no\'s value |\n| `opens` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The view the register opens on: each entry a narrowing on a field the rows show that stands as a chip the reader may take away (the rows still owed), `"me"` a member field holding the reader (the rows mine). One on the status replaces the chips\' opening on the open work |\n| `remove` | `false` | no | The app edits its records and offers no Delete on them (a row\'s \u22EF, its record\'s \u22EF) \u2014 a desk correcting a date of records another app makes and closes; absent, a record the app writes is deleted where it stands |\n| `create` | list of alias (at least one) \\| `false` | no | The fields asked when a row is added; absent, the title, the subtitle and figure no default fills, the natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `export` | `true` \\| list of [Register report](#register-report) (at least one) | no | The rows in view saved as the register\'s report \u2014 its title, a line per thing narrowing the rows, the readings and every row: `true`, one workbook of the register\'s columns; a list, the reports a reader picks from one export menu |\n| `readings` | list of ([Metric](#metric) \\| [Breakdown](#breakdown) \\| [Trend](#trend) \\| [Pivot](#pivot)) (at least one) | no | Readings of the register\'s own entity above its rows, over the rows in view \u2014 the search, the status and the filters narrow them, but a picture\'s own field, read as the register opens on it. A press on a breakdown\'s part, a pivot\'s cell or a figure with `where` narrows the rows by its field, as a filter does |\n\n#### Tiered filter\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A number \u2014 stored, or a formula or rollup reading one \u2014 in one unit for every row |\n| `tiers` | list of number (at least one) | yes | The breakpoints between its bands, ascending, in the field\'s stored unit: under the first, from each to the next, from the last up \u2014 each band worded by the field\'s own format, several picked at once |\n\n#### Sort key\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | The field rows are ordered by |\n| `desc` | `true` | no | Latest or largest first; absent, soonest or smallest first |\n\n#### Register gantt\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | yes | A date of this entity each row\'s bar starts at (a sailing, a hire\'s start, a task\'s start) |\n| `end` | alias | no | The date each row\'s bar ends at, read as `start` is \u2014 two days or two moments; absent, the entity\'s first `due` |\n| `milestones` | list of alias (at least one) | no | Dates of the row marked on its bar\'s line as diamonds, each named by its label (a cut-off, a delivery, the end of free time) \u2014 never `start` or `end` |\n| `planned` | [Gantt plan](#gantt-plan) | no | The plan each row is read against, drawn as a thin bar under its own \u2014 each date read as `start` is; a missing one is the bar\'s own |\n| `progress` | alias | no | A percent of this entity (0\u2013100) filling each row\'s bar as far as the work is done; absent, the bar wears its status\'s tone |\n| `after` | alias | no | A link of this entity to its own rows each row waits on \u2014 it starts once they end (finish-to-start), an arrow from each; a row starting before one ends is drawn in the danger tone |\n\n#### Gantt plan\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `start` | alias | no | The date the row was planned to start at |\n| `end` | alias | no | The date the row was planned to end at (a booked arrival) |\n\n#### Register tab\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `tab` | alias | yes | The tab\'s name: its address, and the key its rows stand under in a report |\n| `label` | text | yes | What the tab holds, in the reader\'s words |\n| `in` | alias | no | A date (stored, or a formula): the tab holds the rows dated within the register\'s period, from its start through its last day, or up to its last minute \u2014 the arrivals of a period |\n| `open` | [Tab open](#tab-open) | no | The tab holds the rows open at the period\'s end: begun before it, and not ended by then \u2014 what is in stock, out on hire or still owed at that moment |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no is this value, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order this tab\'s rows read in, as the register\'s `sort`; absent, the register\'s |\n\n#### Tab open\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `from` | alias | yes | The date a row begins on (stored, or a formula) |\n| `to` | alias | yes | The date a row ends on (stored, or a formula), empty while it is open |\n\n#### Period days\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `days` | integer | yes | How many days through today, at most 366 |\n\n#### Register report\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `label` | text | yes | The report\'s name, in the reader\'s words \u2014 its entry in the export menu and its file\'s title |\n| `description` | text | no | What the report holds, under its name in the menu |\n| `template` | alias | no | A template filled with the report \u2014 `title`, `lines`, `readings`, `at` (when it was made), `dates` (each date filter\'s `from` and `to`, by field), `period` (the tabs\' `from` and `to`), `per` (the value picked), every row in view (at most 20,000) under the entity\'s alias, and each tab\'s under its name: an html one made a PDF, an excel one a workbook; absent, the workbook of the register\'s columns, a sheet per tab |\n| `filename` | text | no | The file\'s name, in the template grammar over one value each \u2014 `title`, `at`, `dates.<field>.from` or `.to` of a date filter, `period.from` or `.to` with tabs, `per` with a `per`, `lines \\| lookup:<n>` \u2014 `Stock {{per}} {{period.to \\| format:"dd.MM.yyyy"}}`; absent, the template\'s name, or the report\'s title |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | The rows the report is of, set on the register as the chips they are before its file is made \u2014 the screen shows what the file holds |\n| `per` | alias | no | A select, a member or a one-link the reader picks one value of from the menu \u2014 the rows narrowed to it, as its chip, and one file of that value |\n| `books` | list of alias (at least one) | no | The rows of these books only \u2014 each the app\'s `entity` or one of its `books`; absent, every book\'s |\n\n<!-- generated:end apps-register -->\n\n<!-- generated:start rules-register -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `register.export-report` | A register\'s reports each have their own label; `per` picks one option, person or one-link row \u2014 never of the status, the folder or a field its `where` fixes. |\n| `register.export-template` | A template an export fills reads at its root only `title`, `lines`, `readings`, `at`, `dates` and its rows under the register entity\'s alias \u2014 an entity named none of those \u2014 and is the document of no act. |\n| `register.column-drawn` | A column never names what the row draws itself \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 and names a field once. |\n| `register.filter` | A filter names a select, a member, a link, a date, a yes/no or a number of the register\'s own rows \u2014 a lookup through one-row links as the field it reads \u2014 never the status (its chips), nor one `where` fixes. |\n| `register.filter-split` | *Noted, never refused:* A filter on a field a breakdown or pivot in the band splits the rows by \u2014 a press on its part narrows by it too; a member\'s filter, which narrows to the reader\'s own rows, is never noted. |\n| `register.filter-unshown` | A filter, a narrowing the register `opens` on, and a press in its band (a breakdown\'s part, a pivot\'s cell, a figure\'s `where`) name a field its rows show \u2014 a column; the row\'s title, the line under it, its picture, status or figure, a meter\'s bound, or the unit or currency a drawn figure is read in; a deadline; a warning its line says; the group or lane it stands under. A narrowing to the reader (`"me"`) needs none. |\n| `register.tiers` | `tiers` band a number read in one unit on every row; a figure read in each row\'s own unit or currency is named bare. |\n| `register.search` | `search` names text, a code, or a link \u2014 matched by its row\'s title. |\n| `register.sort` | Each `sort` key names a field of the register\'s rows that holds an order \u2014 never files \u2014 and no field twice. |\n| `register.tabs` | A register\'s `tabs` each name a tab of their own, never a key a report already holds; a tab\'s `in` and `open` dates are dates of its rows, `open`\'s two different; its `where` and `sort` hold as the register\'s do, and never narrow by the status, which its chips split. Only a table or cards reads tabs, and a band reading\'s `tab` names one of them. |\n| `register.group` | `group` names one value per row \u2014 a single select, a one-row link, one member, a date, a yes/no or a text, a lookup through one-row links as the field it reads \u2014 never the status (its chips); only a table groups, and a gantt into its lanes; grouping by the title needs a subtitle to lead each row. |\n| `register.readings-own` | A register\'s `readings` read its own entity; another entity\'s stand on a dashboard. |\n| `register.readings-restate` | A register\'s metric never sums the row\'s figure or counts the rows without a `where`: the summary line totals the figure and the status chips count the rows. |\n| `register.where-opens` | `where` and `opens` hold the register\'s own selects and yes/nos, and `opens` a member field holding the reader (`"me"`); a field `where` fixes is never a filter nor opened otherwise, and `opens` stays among the options `where` keeps. |\n| `register.roster` | `layout: "roster"` and `roster` go together: the roster\'s entity has one single link to this one and a date on its line, a roster draws no `columns`, `expect` needs `roster` and `of` needs `expect`. |\n| `register.roster-expect` | A roster\'s `expect` is a single select of its rows, and `of` this entity\'s multi-select whose every option is one of `expect`\'s. |\n| `register.gantt` | `layout: "gantt"` and `gantt` go together: `start` and `end` (absent, the entity\'s first `due`) are two different dates of the row, both days or both moments; each milestone another date of it, each planned date read as `start` is, `progress` a percent and `after` a link to rows of the same entity. |\n| `register.lanes` | `layout: "lanes"` and `lanes` go together: `lanes` is a one-row link to an entity with a `records` entry, the rows name a date on their line, a block\'s two ends are read alike (two days or two moments), and `loads` needs `lanes`. |\n| `register.loads` | `loads` names one to 2 numbers a block adds \u2014 never a percent \u2014 each against a number of the lane\'s row, in one unit or two of one dimension. |\n| `register.band-pivot` | *Noted, never refused:* A pivot in a register\'s band: its grid reads on a dashboard, and the band leads with a `metric`. |\n| `register.band-headline` | *Noted, never refused:* A register over rows that move (a status or a deadline) stating no `readings`: the band leads with the job\'s headline number. |\n| `register.remove` | A register\'s `remove: false` hides the Delete its records would offer, so it stands only where the app writes its records. |\n\n<!-- generated:end rules-register -->\n\nThe row is the entity\'s `records` entry: its picture, title and the line under it,\nthen the status, the columns, and the figure at the right; the title, the status and a\ncolumn of days or amounts sort the rows by their header. The status is always the chips, with\ncounts, opening on the rows not closed (every row on a calendar or a lanes board, whose\nwindow of time narrows them) \u2014 never one of `filters`. `filters` are the ones a reader\nnarrows by every day \u2014 none to three, never a quota \u2014 in order of use, each a field the\nrows show (a column, a part of the row), each on the line\nwith the search and the chips, the grouping one\nchip at its end; a member filter offers "mine". A date filters by a range of days \u2014 of\nminutes on a `datetime` field, its end not held, so back-to-back shifts read each row\nonce \u2014 and can keep the rows holding no date too (in the yard at a moment: in before it,\nout after it or not yet). A number filters by a range typed in its\nunit or currency \u2014 a figure read in each row\'s own, in the one picked beside it \u2014 with a\nslider over a percent or a figure against a constant `limits`; `{ "field", "tiers" }`\noffers the bands its breakpoints cut in place of a range (`[1000, 5000]` on a weight in\nkg: under 1.000 kg, 1.000\u20135.000 kg, 5.000 kg and over), refused on a figure read in each\nrow\'s own unit. A threshold with a name is a formula yes/no, never a range. A\ncolumn the row already draws is refused. `sort` is one key or a list of keys, most\nsignificant first, each breaking the ties of the ones before it \u2014 a shipping line in its\nselect\'s option order, then the arrival day soonest first:\n`[{ "field": "shipping_line" }, { "field": "arrived_on" }]`; a header\'s press leads them.\nAbsent `sort`, rows open soonest deadline first where the row has a `due`, else newest first. `search` names the fields\nthe search box matches \u2014 a link by its row\'s title \u2014 and a pasted list searches each\nline, naming the lines no row matched; absent, it matches every word of the row.\n`export` saves the register as its report \u2014 the title, a line per chip, the search and\nthe moment it was exported, the readings, and every row in view, at most 20,000 of them.\n`export: true` saves it as a workbook, the readings on a sheet before the rows. A list\nstates the reports the business sends \u2014 one a button, several one menu \u2014 each a `label`,\na `description`, and a `template` filled with `title`, `lines`, `readings`, `at` (the\nmoment it was made), `dates` (each date filter\'s `from` and `to` by field, a day or a\nmoment as the filter bounds the rows) and the rows under the entity\'s alias (an html one\nmade a PDF, an `excel` one a workbook; absent, the workbook). A report\'s `filename` names\nits file in the template grammar over the same keys but the rows \u2014\n`"Stock {{dates.arrived.to | format:\\"dd.MM.yyyy\\"}}"`; absent, the template\'s name, or\nthe label. A report of some rows (`where`), or of one value the reader picks (`per`: one\nline\'s file), first narrows the register as its chips, so the screen shows what the file holds.\n`readings` read the register\'s own rows above them: the rows in view, so the search,\nthe status and the filters narrow each one, but a picture\'s own field, which it reads as\nthe register opens on it. They are optional \u2014 state them where a\npicture answers something the rows cannot \u2014 and stand under the title in one compact\nband: lead with the job\'s headline number (a `metric`), then at most the mix (a\n`breakdown`) and the movement (a `trend`) \u2014 a `pivot` stands under the band as a whole table, every line and its totals. A picture\nstands only where it splits the rows in view into two parts or more, one of them\nholding two rows or more; a split whose rows\nall fall in one part, and a lone figure with no picture beside it, are said on the\nsummary line instead, so a band stands only holding a picture or two figures. The summary line already totals the\nfigure over the rows in view and the status chips count them, so a `metric` summing the\nfigure, or counting the rows of an entity with a status, is refused unless its `where`\nnarrows it.\n\n`where` keeps the rows the app works, read so on the server and never a chip: a\npurchase desk over orders of both directions states `"where": { "direction":\n["purchase"] }`, and a row added there is written holding the one option (a select kept\nat several is asked among them). `opens` is the view the register opens on, each entry a\nchip the reader may take away: a cashier opens on `"opens": { "owes": true }`. Both take\na select\'s options or a yes/no\'s value, stored or a formula; `opens` on the status is\nthe chips\' opening choice. `opens` also takes `"me"` on a member field \u2014 `"opens": {\n"assignee": "me" }` opens on the rows assigned to whoever reads, the server reading the\nreader, never the page; `where` refuses it, since it fixes the rows for every reader\n(who may read a row at all is the entity\'s `read_scope`).\n\n`layout: "calendar"` places each row on its day \u2014 a month, a week, a day or a list (a\nphone reads the list or a day): at its hour where the date holds one, over its span\nwhere the row\'s line holds a second date after it, its line\'s other words and the\nregister\'s `columns` after its title. The calendar reads the window in view, and its\ncount and the status chips count that window, opening on every row.\n\n`layout: "lanes"` with `lanes` naming a one-link draws each row of the linked entity as\na lane (a chair, a machine, a room), empty ones too, and each register row as a block\nfrom the first date on its line to the second: over the hours of one day where the first\nholds its hour, else over the days of one day, a week or a month. A block reads the row\'s\nname and the first of its `columns`; a free stretch between blocks adds a row there, its\nlane and its start set, and its end where the next block bounds it. A phone lists each\nlane\'s blocks and free stretches in clock order. Rows naming no lane stand first, in a lane\nof their own; where the app writes the link, each block moves to another lane from its menu.\nInside a folder (`scope`), the lanes are the ones whose own one-link names that folder \u2014 a\nbranch\'s rooms, never every branch\'s. A span\'s end holds its day unless `records` names it\nthe day the row `frees`.\n\n`loads` names what a lane carries: `{ "weight": "payload", "volume": "box" }` sums each\nblock\'s number and reads it against the lane row\'s own, at most two. A lane holds its\nblocks while they stand, so it reads the most they add up to at one time in the window \u2014\na day\'s rows together \u2014 named, past its bound, by how much and on which day or at which\nhour. The two are in one unit, or two of one dimension (kg against t); a block then reads\nwhat it adds, and the move menu reads each lane as it would stand with the block on it.\nA bound a single row must keep (no parcel heavier than its van) is a `write_rules` `max`\nthrough the link, which refuses the move.\n\n`layout: "gantt"` with `gantt` draws each register row as one bar on one axis of time:\n`{ "start": "etd", "end": "eta", "milestones": ["cut_off"], "planned": { "end": "booked_eta" },\n"progress": "done", "after": "waits_on" }`. `end` defaults to the entity\'s first `due`, and an\nentity with neither is refused; `start` and `end` are two dates of the row, both days or both\nmoments, and each planned date is read as `start` is. `progress` is a percent (0\u2013100);\n`after` is a link to rows of the same entity, one or several, each ending before the row\nstarts. The bars stand in the lanes of the register\'s `group`. Where the app writes the\nrow and a person writes a bar\'s date, the bar is dragged through the row\'s save, which then\nwrites those two dates though no section places them; a computed date, one an act sets, or\nan app with `writes: "children"` drags nothing.\n\n`layout: "roster"` draws the register\'s rows down the side and the days of a week or a\nmonth across; `roster` names the entity whose rows fill the days \u2014 each stands on one\nregister row by its single link to it, and on the first date of its line (a shift on a\nperson\'s day, a run on a vehicle\'s), on every day through the second date where its line\nholds one. A cell reads its row\'s status, else the first single select on its line (a\nshift\'s kind), several rows their count; a week\'s cells say their words and one more\nfact of the line, a month\'s keep their marks. A day holding none stays empty: nothing\nwas due, which is never an absence. A filled cell opens its row; an empty one adds a row\nthere, the register row and the day set, asking what any add of it asks \u2014 unless no\nperson writes those rows (`writes: false`). With `expect` (a single select of those\nrows) the columns are its options in place of days \u2014 a checklist of papers \u2014 and `of`\n(this entity\'s multi-select) names the ones each row expects: a gap among them reads as\none and adds its row, the others stand blank, and each row reads what it holds of them,\n`2/3`. A roster states no `columns` and needs no `readings`: its legend counts each\nstate in view.\n\n```jsonc\n"register": { "layout": "roster", "roster": "run", "filters": ["operator"] }\n```\n\nAn app whose job reads its rows otherwise than the entity\'s `records` restates them in\nits own `row` \u2014 the cashier\'s figure is what is still owed, its line the service and the\nday. The subtitle, figure and image it states replace the entity\'s wherever that app\ndraws a row of it; every other app reads `records`, and the title is the entity\'s.\n\n### Readings\n\nA reading is one number, or one picture of numbers, over rows. Where it stands decides\nwhich rows: a dashboard reads every row, a register\'s `readings` the rows in view, a\nrecord\'s reading block the child rows under the record. The picture is the runtime\'s,\nchosen from what the reading means \u2014 no key picks a chart.\n\n```jsonc\n{ "metric": "visit", "value": "fees", "where": { "paid": false }, "over": "arrived_on", "label": "Unpaid" }\n{ "breakdown": "visit", "by": "line" }\n{ "trend": "visit", "over": "arrived_on", "value": "fees" }\n{ "pivot": "visit", "rows": "size", "columns": "line" }\n{ "list": "visit", "where": { "stage": ["in_yard"] }, "columns": ["line"], "app": "gate", "label": "In the yard" }\n{ "list": "task", "where": { "assignee": "me" }, "label": "My tasks" }\n```\n\nA reading\'s `where` keeps rows by a select\'s options, a yes/no\'s value, or `"me"` on a\nmember field \u2014 the rows that name whoever reads, read so on the server.\n\n<!-- generated:start apps-readings -->\n\n#### Metric\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n#### Breakdown\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n\n#### Trend\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n\n#### Pivot\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n\n#### List\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `list` | alias | yes | The entity whose rows are listed, each in its `records` anatomy |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `columns` | list of alias | no | Fields read after each row\'s title |\n| `app` | alias | no | An app of this model over the same entity that a row opens in |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n\n<!-- generated:end apps-readings -->\n\n<!-- generated:start rules-reading -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `reading.value` | A reading sums a number, never a percent \u2014 percents do not add up. |\n| `reading.split` | `by`, `rows` and `columns` split by one value each row holds \u2014 a single select, a one-row link, one member or a yes/no, stored or a formula, a lookup through one-row links as the field it reads \u2014 and a pivot crosses two different fields. |\n| `reading.over` | `over` is a date: the row\'s own, stored or a formula, or its parent\'s through a lookup over a one-row link. |\n| `reading.where` | A reading\'s `where` keeps rows by a select\'s options, a yes/no\'s `true` or `false`, or a member field holding the reader (`"me"`). |\n| `reading.target` | `target` reads a value nothing bounds \u2014 a value `limits` bounds reads against the sum of its bound. |\n| `reading.better` | `better` needs `over`: a change is read over the periods its date places the rows in. |\n| `reading.list-app` | A list\'s `app` is a register app of this model over the list\'s own entity. |\n\n<!-- generated:end rules-reading -->\n\nA reading counts its rows, or sums the number `value` names \u2014 never a percent. `by`,\n`rows` and `columns` split the rows by one value each: a single select, a one-row link,\none member, or a yes/no \u2014 stored or a formula, its parts read as its label and the\nothers. `over` is a date, the row\'s own (stored or a formula) or its parent\'s through a\nlookup over a one-link: a trend\'s periods, a metric\'s recent periods beside its number,\nand what a dashboard\'s period windows. `ahead: true` reads a trend forward from the\ncurrent period over the rows still to come. `where` keeps rows whose select holds one\nof the options, or whose yes/no is the value, every entry ANDed \u2014 a band leads with the\njob\'s exception as a formula\'s yes/no (what is late, what is short), never a stage the\nstatus chips already count. A metric summing a value `limits` bounds reads against the\nsum of its bound (`10 / 14`); `target` reads one against a number nothing bounds. A\nmetric\'s change is green where it moves the good way: a sum\'s rise, a count kept by\n`where` falling; `better` states the other way where that reads wrong \u2014 money going out\n(`"better": "down"`). A register reads only its own entity; a `list` stands only on a\ndashboard.\n\n### Record\n\n```jsonc\n"record": {\n "door": "page",\n "sections": [\n { "title": "Details", "fields": ["size", "line", "built_on"] },\n { "title": "Gate in", "at": ["arriving", "in_yard"], "fields": ["arrived_on", "truck"],\n "blocks": [{ "rows": "fee", "where": { "leg": ["in"] }, "expect": "kind", "columns": ["amount", "paid_by"] }],\n "acts": ["gate_in_paper"] },\n { "title": "Sell to a buyer", "description": "Moves the container to the buyer\'s stock once it has left.", "acts": ["sell"] }\n ]\n}\n```\n\n<!-- generated:start apps-record -->\n\n#### Record page\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `door` | `"page"` \\| `"drawer"` \\| `"beside"` | no | A page for work read at length; a drawer for rows worked one after another from the register; `beside` for rows worked one after another while the list stays in view \u2014 the register\'s list and the open record side by side, over a table register alone. Each stacks its sections, and from three sections on navigates them \u2014 a page by a rail, a drawer by tabs; absent, a page where a section has `at` or is a `page`, the record has more than three sections or one stands beside the rest (`side`), else a drawer |\n| `sections` | list of [Section](#section) (at least one) | no | The record in the order its work reaches it \u2014 each section its fields, then its blocks, then its acts. Absent, one section of every field a person writes that no other place shows |\n| `comments` | `true` | no | The record\'s comment thread beside the sections \u2014 who wrote what and when, with a composer \u2014 stated only where the job\'s people discuss a record or the owner asks for one. Absent, it is drawn nowhere |\n| `history` | `true` | no | The record\'s status history beside the sections \u2014 each move, when and who \u2014 stated only where the owner asks for an audit trail of the moves: the rail already marks the steps passed, and the entity\'s `status.history` is written either way. Absent, it is drawn nowhere |\n\n#### Section\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `title` | text | yes | What the section is, in the reader\'s words \u2014 its heading, and its name on the record\'s rail or tab |\n| `description` | text | no | One sentence under the title: what the section is for, or what its act does |\n| `at` | list of alias (at least one) | no | Options of the record\'s status (or its milestones) during which this section is the work now \u2014 a step of the path the record moves through: its rail item reads now while the status holds one and this app has work there, done once the record\'s history says it went through one \u2014 or it is a step of the path before where that history opens (with none, where the record stands) that no act moves the work past \u2014 and it stands before the section that is the work now, and waiting otherwise; a section every option of which ends the work, past the path\'s end or entered by its own act, is an exit: no item and nothing drawn until the record stands in it, then set apart after the steps and never one; A page opened from the register opens at it. It never locks what the section holds: its fields are written as the write rules and checks allow at any stage. An act it names is offered during these alone: its `when` holds the status to them, or on milestones it stamps the step after one of them \u2014 a detour and an exit are sections of their own, a detour stated before the step it returns to |\n| `fields` | list of alias (at least one) | no | The record\'s own fields in this section, in the order they are read \u2014 never one the header draws: the row\'s title, subtitle, image, status or figure |\n| `blocks` | list of [block](#blocks) (at least one) | no | What the section holds besides its own fields, under them, in order |\n| `acts` | list of alias (at least one) | no | Acts of this app drawn under the section\'s fields, their asks as more of them and what each lacks said above its button; the header draws no act, so every act of the record itself is named by one section \u2014 an act of a child (`of`) stands on the child\'s row and is never named here. An act moving the status, offered at ONE step (the stage its `when` holds), stands at that step\'s foot when it moves the work forward \u2014 to a later option, an exit too \u2014 or back to a step the work passes anyway; one offered at several steps, or moving back into a detour, stands in the section it enters, which draws it while the work waits \u2014 an exit\'s in the header\'s \u22EF, since an exit stands only once the record is in it. A correction \u2014 `danger` and not one of its step\'s outcomes (no other status move offered at the same moment) \u2014 waits in the section heading\'s \u22EF, and one no section names in the header\'s \u22EF; every other act is named by a section |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `side` | `true` | no | The section the rest are worked from, drawn in a pane beside them rather than in their scroll \u2014 the photos looked at, the pool picked from, the paper typed off; at most one on a record, which opens on a page door. It holds what any section holds |\n| `page` | `true` | no | A page of its own, off the record\'s scroll: its own heading and a way back, reached from the record\'s rail after the scroll\'s sections, or from a link at its place where the record has no rail \u2014 for what makes the record worse stacked in it: a long list of related rows, a workspace of its own. Only on a page door, never a step (`at`) |\n\n<!-- generated:end apps-record -->\n\n<!-- generated:start rules-record -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `record.placed-once` | A field, a block and an act each stand once on a record: the header draws the title, subtitle, image, status (or milestones), figure and folder, and a field\'s one other place is a section\'s `fields`, a files block or a text block \u2014 save the image, which one files block may draw again, whole. |\n| `record.foot-total` | A rows block\'s foot totals the child\'s figure and each summed column over the rows it lists; the record\'s rollup summing that field over those rows (the same `where`) is that foot, never also one of a section\'s `fields`. |\n| `record.fact-restates` | A section\'s field never looks up what its link already reads where it stands: the linked row\'s title and line beside the link, the figure or bound its meter reads on the header\'s line. |\n| `record.section-holds` | A section holds fields, blocks or acts. |\n| `record.at` | `at` names options of the record\'s stored status, or its milestones; a record with no status has no steps. |\n| `record.page` | A section is a `page` only on a page door \u2014 never a drawer\'s or one beside the list \u2014 and never a step (`at`), its title naming an address no other page shares (a letter or a digit at least), and never every section. |\n| `record.side` | At most one section stands beside the rest (`side`), on a page door, and never also a page of its own. |\n| `record.beside` | A `door: "beside"` record opens beside a table register \u2014 never a board, cards or a calendar, which draw no list to stand beside. |\n| `record.history` | `record.history` draws a status history the entity records (`status.history`); no block reads that history. |\n| `record.section-acts` | A section\'s `acts` name acts of the record itself: an act of a child (`of`) stands on the child\'s row, and one over the register\'s rows (`on`) on the register. |\n| `record.act-in-section` | Every act of the record stands in the `acts` of the section whose work it is; only a correction no section owns \u2014 `danger`, and no other status move offered at the same moment \u2014 waits in the header\'s \u22EF. |\n| `record.move-home` | A status move offered at one step stands at that step\'s foot when it moves the work forward, to an exit, or back to a step it passes anyway; one offered at several steps, or moving back into a detour, stands in the section `at` the status it enters. |\n| `record.act-staged` | An act at a staged section\'s foot is offered only during that section\'s steps: its `when` holds the status to options the section names \u2014 on milestones, it stamps the step after one of them. |\n| `record.act-hidden` | An act is never offered only while its section\'s `when` hides the section. |\n\n<!-- generated:end rules-record -->\n\nThe record is its `sections`, stacked in the order the work reaches them. A section is\nits fields, the acts under them, then its blocks \u2014 the acts at its foot instead where one\nreads what a block holds or none of its fields \u2014 and holds at least one of them. From\nthree sections on, the record is navigated by their titles: a page by a rail that jumps\nto each (a strip of the same names on a phone), a drawer by tabs, the companion the last;\nwith fewer, the sections just stack. `description` is one sentence under the title.\n`at` names the status options (or the milestones) during which the section is the work\nnow \u2014 only on a record that truly moves through those stages: the rail marks it done,\nnow or waiting (an item of no `at` is its title alone), and the section stands even holding\nnothing yet \u2014 it never hides a field, locks it or gates an act, and a record opens at its\nhead. `page: true` makes a section a page of its own under the\nrecord\'s, off its scroll, with a way back \u2014 rarely, for a long related list or a workspace\nof its own that would make the record worse stacked; never on a drawer or a step. The\nheader draws no act: every act on the record is named by the section whose work it is, and one no section names is refused, naming its likely\nsection \u2014 only a correction no section owns waits in the header\'s \u22EF beside Delete. An act\nmoving the status and offered at ONE step (its `when`) stands at that step\'s foot when it\nmoves the work forward \u2014 to the next step, or to an exit later in the status\'s options\n(Reject beside Approve) \u2014 or back to a step the work passes anyway. One offered at SEVERAL\nsteps, or moving back into a detour, is named by the section it enters: a detour draws it and its\ngaps alone while it waits. A `danger` act is a correction \u2014 waiting in the \u22EF of what it acts\non: its section\'s heading, its row, the header\'s where no section names it \u2014 unless it moves\nthe status while another status move is offered at the same moment; then it is one of that\nstep\'s outcomes, a button. Cancel offered at Held and at Confirmed, beside Confirm, is an\noutcome into an exit: state `{ "title": "Cancelled", "at": ["cancelled"], "fields": ["reason"],\n"acts": ["cancel"] }`. An exit stands only once the record is in it, its reason there and its\nrail item apart from the path; until then Cancel waits in the header\'s \u22EF. `comments: true`\nputs the record\'s comment thread beside the sections, and `history: true` its status history \u2014\nthe history never as a section. Both are opt-in: state them only where the job\'s people\ndiscuss a record, or the owner asks for an audit trail of its moves.\n\nA field, a block and an act is each placed ONCE on a record \u2014 the header, one section;\na second place is refused. The header draws the entity\'s `records` title, subtitle,\nimage, status (or milestones) and figure, so none of them is named again in a section\'s\n`fields` or the register\'s `columns` \u2014 save the image, which one `files` block may draw\nagain, whole at reading size (an incident\'s photo, a scan), the header keeping its mark. Refused too: a section holding nothing, an `at`\non a record with no status, and a section\'s act that is a child\'s or runs over the\nregister\'s rows.\nAbsent `sections`, one section holds every field a person writes that the header does\nnot show. An editable field rests as its control and saves as it changes; everything\nelse is plain text. A section or a block with `when` is shown only while each named\nselect of the record holds one of its options. The one layout serves every entity: a\ncustomer or a product is sections without `at`.\n\n### Blocks\n\n```jsonc\n"blocks": [\n { "rows": "fee", "columns": ["kind", "amount"] },\n { "rows": "fee", "where": { "leg": ["out"] }, "columns": ["kind", "amount"], "under": "invoice" },\n { "agenda": "booking", "columns": ["guide"], "start": "arrival" },\n { "timeline": "note" },\n { "files": ["photos", "papers"] },\n { "text": "remarks", "when": { "stage": ["held"] } },\n { "metric": "fee", "value": "amount", "label": "Fees" },\n { "breakdown": "fee", "by": "kind" },\n { "trend": "fee", "over": "charged_on", "value": "amount" },\n { "pivot": "fee", "rows": "kind", "columns": "method" }\n]\n```\n\n<!-- generated:start apps-blocks -->\n\n#### Rows block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `rows` | alias | yes | A child entity whose rows belong to this record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s fields read as columns after its title \u2014 never what its row draws (subtitle, image, status, figure, or the bound its figure\'s meter reads against), though an `expect` block\'s figure is a column, filled in place. |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `where` | map of alias \u2192 list of alias (at least one) | no | Only the child rows whose single select holds one of these options \u2014 every entry ANDed, read so on the server; a row added here holds them (one leg\'s fees, of a record holding both legs\'). Each select is one the child requires, so no row stands in no block |\n| `expect` | alias | no | The child\'s title when the record holds one row per value of it \u2014 a single select (one per option, in order) or a one-row link to a catalog (one per row the link may point at): a value not yet filled reads as its own empty line, and no value holds two rows. The block\'s `columns` name at least one field a person fills in place |\n| `of` | alias | no | With `expect` on a select: the record\'s own multi-select holding the options this record expects a line for \u2014 the options it holds, in order, rather than every option. Its options are among the title\'s |\n| `under` | alias | no | The child\'s one-link to a document that covers its rows (an invoice over its fee lines), whose many-link points back and which links to this record: each document stands as a heading over the lines it covers; a filled line no document covers says so on its own row with a make for one document over it, and two or more waiting take a combined make in the block\'s heading. Never also a rows block of the document |\n| `filters` | list of (alias \\| [Tiered filter](#tiered-filter)) (at most 3) | no | The child\'s fields the reader narrows the block\'s rows by, as a register\'s `filters` \u2014 none to 3, each a field its rows show (a column, or a part of the row), its status among them where the business narrows by it \u2014 never on a task list, whose status chips narrow it |\n| `search` | list of alias (at least one) | no | The child\'s fields the block\'s search box matches (a link by its row\'s title), a pasted list line by line; absent, the block has no search box |\n| `group` | alias | no | The child\'s field the block\'s rows open grouped by, as a register\'s `group` \u2014 the reader\'s grouping starting there; absent, ungrouped |\n| `sort` | [Sort key](#sort-key) \\| list of [Sort key](#sort-key) (at least one) | no | The order the block\'s rows open in, as a register\'s `sort` \u2014 within each group where the block groups them; absent, the child\'s first deadline soonest first, else the newest first |\n| `edits` | list of alias (1\u20132) | no | The child\'s fields each line holds as its control, written where the line stands (a quote line\'s sell price) \u2014 a number, a date, words or one option the row\'s save writes, each a column or the figure, at most 2; every other value reads, and the row opens to write it |\n| `remove` | `false` | no | This list offers no Delete on its rows (its open row and \u22EF); they are deleted where another list of them or their own record offers it \u2014 e.g. lines that are a selection of rows kept elsewhere |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Timeline block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `timeline` | alias | yes | A child entity read as a log of dated entries, newest first, with a composer where the app adds entries, each corrected and removed where it stands as a rows block\'s row (entries planned ahead are an `agenda`) \u2014 never the record\'s status history, which `record.history` draws beside the record |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, the subtitle and figure no default fills, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Agenda block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `agenda` | alias | yes | A child entity read as planned entries on a time axis: by day, soonest first, each entry with its picture and the day that is today marked. The day is the first date on the child\'s row line (its `records` title or subtitle); within a day entries follow a datetime there, else the first single select after the date \u2014 its options in the day\'s order |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `columns` | list of alias | no | The child\'s facts read on an entry\'s line after its title \u2014 never what its line already draws (subtitle, image, status, figure) |\n| `create` | list of alias (at least one) \\| `false` | no | The child\'s fields asked when a row is added here; absent, its title, its picture, the subtitle and figure no default fills, the block\'s columns, its natural key and every required field \u2014 each a person writes; false, rows are not added here |\n| `start` | alias | no | The record\'s date that is the first day: each day\'s heading counts from it (Day 1, Day 2); absent, the weekday and the date alone |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Files block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `files` | list of alias (at least one) | yes | Files fields of this record, read as pictures and documents |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Text block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `text` | alias | yes | A long text field read as prose |\n| `title` | text | no | The block\'s heading; absent, the child entity\'s or the field\'s own label |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Metric block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `metric` | alias | yes | The entity whose rows are counted, or summed, into one number |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `target` | number | no | The number it is read against, where no `limits` bounds its value (a bounded value is read against the sum of its bound) |\n| `better` | `"up"` \\| `"down"` | no | Which way its change over the recent periods `over` reads is good; absent, a sum\'s rise and a count kept by `where` falling |\n| `label` | text | yes | What the reading is, in the reader\'s words |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Breakdown block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `breakdown` | alias | yes | The entity whose rows are split into parts |\n| `by` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) each row is counted under |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the `by` field\'s label |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Trend block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `trend` | alias | yes | The entity whose rows are counted, or summed, per period |\n| `over` | alias | yes | The date each row falls on \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link |\n| `ahead` | `true` | no | Counts forward: the current period first, then the ones after it, over the rows dated today or later (arrivals by their ETA); absent, the periods up to today |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the value\'s label, else the entity\'s |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n#### Pivot block\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `pivot` | alias | yes | The entity whose rows are counted, or summed, in a grid |\n| `rows` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) down the side |\n| `columns` | alias | yes | The single select, one link, member or yes/no (stored, a formula, or a lookup of one through one-row links) across the top |\n| `value` | alias | no | A number field summed over the rows; absent, the rows are counted |\n| `where` | map of alias \u2192 (list of alias (at least one) \\| boolean \\| `"me"`) | no | Only the rows whose select holds one of these options, whose yes/no (stored, or a formula) is this value \u2014 each its own, or looked up through one-row links \u2014, or whose member field holds the reader (`"me"`) \u2014 every entry ANDed |\n| `tab` | alias | no | On a register\'s band, the tab whose rows it reads, whichever tab is open; absent, the open tab\'s |\n| `over` | alias | no | The date that places a row in time \u2014 its own (stored, or a formula), or its parent\'s through a lookup over a one-link: a dashboard\'s period windows the rows by it (a reading without one reads the rows as they stand now), and a metric reads its recent periods beside the number |\n| `label` | text | no | What the reading is, in the reader\'s words; absent, the two fields\' labels |\n| `via` | alias | no | The child\'s link to this record, where it has more than one |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | Shown only while each named select of the record holds one of these options, and each named yes/no (stored, or a formula) is this value |\n\n<!-- generated:end apps-blocks -->\n\n<!-- generated:start rules-block -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `block.child-link` | A block reads a child linked to the record; `via` names the link where the child links more than once. |\n| `block.column-drawn` | A child block\'s column never names what the child\'s row draws \u2014 its title, subtitle, image, status, figure, or the bound its figure\'s meter reads against \u2014 nor its link back to the record, and names a field once; an `expect` block\'s figure is its filled-in-place column. |\n| `block.title-back` | Where a child\'s title is its link back to the record a block lists it under, its first other subtitle names it there \u2014 a text, a code, a select, a link or a member, never a number or a date. |\n| `block.where` | A rows block\'s `where` names single selects the child requires, by options they hold; one held at a single option is never also a column. |\n| `block.under` | `under` names the child\'s one-row link to a document entity that pairs a many-link back and is read under this record by its one link; the lines\' columns never read the document (its heading does) nor hold long text. |\n| `block.under-once` | Documents drawn over their lines (`under`) are never listed again as a block of their own. |\n| `block.edits` | A rows block\'s `edits` name at most two of the child\'s columns or its figure that the row\'s own save writes \u2014 a number, a date, a few words or one option \u2014 never on an `expect` block, whose lines are filled in place already. |\n| `block.remove` | A rows block\'s `remove: false` hides the Delete its rows would offer, so it stands only where the app writes those rows. |\n| `block.narrow` | A rows block\'s `filters`, `search`, `group` and `sort` follow the register\'s rules over the child\'s rows, its status among them but on a task list (its chips); never on the link back to the record or a select its `where` holds at one option, never on an `expect` block, and an `under` block is grouped by its documents. |\n| `block.expect` | `expect` names the child\'s title \u2014 a single select or a one-row link a person writes, never the link back \u2014 on a child added in the block; the block\'s `columns` name at least one field a person fills in place, and its `create` takes the file where the child is pictured by one. |\n| `block.expect-fills` | An expected line is made from its key and its first filled column, so every other field the child requires has a default, a start or a `default_from`, or is read first. |\n| `block.expect-of` | A block\'s `of` needs `expect` on a select, and names the record\'s multi-select whose every option is one of the title\'s. |\n| `block.agenda` | An agenda\'s child names a date on its row line (`records` title or subtitle); `start` is a date of the record. |\n| `block.timeline` | A timeline\'s child holds a date to place its entries by. |\n| `block.field-kind` | A files block names files fields of the record, and a text block a text field. |\n| `block.read-scope` | A child read under a record whose rows a `read_scope` narrows states its own \u2014 a row rule is not inherited, save the status history\'s, which takes its record\'s. |\n| `block.agenda-note` | *Noted, never refused:* Rows each on a day and within it (at an hour, in a part of the day) listed as a table: an `agenda` draws them by day. |\n| `block.under-note` | *Noted, never refused:* Rows holding a paper that cover another block\'s lines, listed apart with no add of their own stated: `under` draws them over the lines they cover. |\n\n<!-- generated:end rules-block -->\n\nA `rows`, `agenda` or `timeline` block reads a child entity through its link to the\nrecord \u2014 `via` names it where the child links more than once. A rows block adds rows\n(asking the child\'s title, its picture, the block\'s columns and its required fields)\nand opens each in a drawer. An `agenda` draws planned entries by day, soonest first:\nthe first date on the child\'s row line is the day, a datetime there or the first single\nselect after the date orders a day, and `start` \u2014 the record\'s date \u2014 is day 1. `under`\nnames the child\'s one-link to a document covering its lines (an invoice over its fees,\nits many-link paired back, linking to the record): each document heads the lines it\ncovers, the uncovered lines first \u2014 the record never lists the documents again as a\nblock of their own. A filled line no document covers says so on its own row, where one\npress makes a document over that line alone, its figure starting at the line\'s amount;\nwhere two or more wait, the block\'s heading makes one over those ticked in its dialog.\nA timeline reads its child\'s first date as the moment and its member field as who, with\na composer where the app writes it. An agenda\'s or a timeline\'s entry with no day yet is\na plan, listed first and dated in place. `create` names the child\'s fields an add here asks \u2014\nthe link to this record is filled, never asked \u2014 and `false` adds none. `expect`\nnames the child\'s title where the record holds one row per value of it \u2014 a select\'s\noptions, or the rows a link to a catalog may point at (`options_where`): every value\nis a line, one not yet filled a quiet line whose first column makes its row \u2014 or, where\nthe child is pictured by a file a person brings (its `records` `image`), a placeholder\nnaming the paper whose upload makes the row with its file \u2014 the add\noffers only the values not yet held, and every write path refuses a second row of one\nvalue under one record. A field that starts from the catalog row (`default_from`)\nshows that value as its hint, taken with one press. `where` keeps only the child rows\nwhose single select holds one of its options \u2014 read so on the server, a row added\nthere holding them, its lines expected once per value among them (one leg\'s fees\nbeside the other\'s), and its foot totalled by the record\'s sum narrowed alike. A `metric`, `breakdown`, `trend`\nor `pivot` block is a reading (\xA7 Readings) of a child entity\'s rows under this record,\nread only \u2014 `via` names the link where the child has more than one.\n\n### Tasks\n\nAn entity whose `records` states `"task": true` \u2014 its status a stored select with `closed` \u2014 is\nwork someone finishes. Every table of its rows draws it alike \u2014 a rows block of the record it\nbelongs to, its own register, the rows filed under one of its rows (its steps) \u2014 each row led by a\nring. Ticking the ring moves the row to a closed option by the one write the app already has for\nthat move \u2014 the act of that entity setting a closed option (its first preferred), offered on an\nopen row and asking nothing, else its own save where people write the status \u2014 stamping what\nthat act stamps and appending the status history. Unticking runs the act moving a closed row\nback (its `when` holding a closed option), else the save back to the status\'s default. A move no\nwrite reaches leaves the ring disabled, never hidden, and the check notes it. The status is moved\nin place on the row by the same moves; every other field is edited where the row opens. A\nfiles column reads on a task\'s line as how many files it holds. An act `of` the child with\n`on: "rows"` completes several at once: the list\'s heading offers to select rows, and the act\nruns on those picked. A block with `expect` or `under` draws its own lines, never rings.\n\n```json\n{\n "entities": [\n {\n "alias": "project",\n "label": "Projects",\n "singular": "Project",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "lead", "label": "Lead", "type": "select_member" }\n ]\n },\n {\n "alias": "task",\n "label": "Tasks",\n "singular": "Task",\n "fields": [\n { "alias": "title", "label": "Title", "type": "text", "required": true },\n {\n "alias": "project",\n "label": "Project",\n "type": "select_record_link",\n "target_entity": "project",\n "cardinality": "one",\n "required": true\n },\n { "alias": "assignee", "label": "Assignee", "type": "select_member" },\n {\n "alias": "status",\n "label": "Status",\n "type": "select",\n "required": true,\n "options": [\n { "alias": "todo", "label": "To do", "color": "slate" },\n { "alias": "done", "label": "Done", "color": "green" }\n ],\n "default": ["todo"]\n },\n { "alias": "due", "label": "Due", "type": "date", "format": "date" },\n { "alias": "done_on", "label": "Done on", "type": "date", "format": "date" }\n ]\n }\n ],\n "records": {\n "project": { "title": "name" },\n "task": {\n "title": "title",\n "subtitle": ["assignee"],\n "status": { "field": "status", "closed": ["done"] },\n "due": ["due"],\n "task": true,\n "starts": { "assignee": "me" }\n }\n },\n "apps": [\n {\n "alias": "projects",\n "name": "Projects",\n "entity": "project",\n "record": {\n "sections": [{ "title": "Work", "fields": ["lead"], "blocks": [{ "rows": "task", "columns": ["due"] }] }]\n },\n "acts": [\n { "alias": "complete", "label": "Complete", "of": "task", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "reopen", "label": "Reopen", "of": "task", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } },\n { "alias": "complete_all", "label": "Complete", "of": "task", "on": "rows", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } }\n ]\n },\n {\n "alias": "my_tasks",\n "name": "My tasks",\n "entity": "task",\n "register": {\n "opens": { "assignee": "me" },\n "readings": [{ "metric": "task", "where": { "status": ["todo"] }, "label": "Open" }]\n },\n "record": { "door": "drawer" },\n "acts": [\n { "alias": "finish", "label": "Finish", "when": { "status": ["todo"] }, "set": { "status": "done", "done_on": "now" } },\n { "alias": "undo", "label": "Undo", "when": { "status": ["done"] }, "set": { "status": "todo", "done_on": null } }\n ]\n }\n ]\n}\n```\n\n### Acts\n\n```jsonc\n"acts": [{\n "alias": "gate_out", "label": "Gate out",\n "when": { "stage": ["in_yard"] }, "requires": ["release", "seal"],\n "asks": ["seal", { "input": "note", "label": "Note", "type": "long_text" }],\n "set": { "stage": "gone", "left_on": "now", "left_by": "me", "remark": "input:note" },\n "confirm": true\n}]\n```\n\n<!-- generated:start apps-acts -->\n\n#### Act\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `alias` | alias | yes | The act\'s name within its app \u2014 its workflow is `act_<alias>` |\n| `label` | text | yes | The verb, as the reader says it |\n| `on` | `"record"` \\| `"rows"` \\| `"view"` | no | `rows` runs on the rows ticked in the register \u2014 with `of`, on the child rows ticked together in a rows block of the record; `view` on every row the register shows (offered while the view is narrowed to its `when`; it states no `requires`); absent, on one record (its page and its row\'s menu) |\n| `of` | alias | no | A rows block\'s child entity the act works on: offered on each child row \u2014 or, `on: "rows"`, on the rows ticked together in the block \u2014 its conditions and its write that row\'s |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The act works only while each named select holds one of these options, and each named yes/no (stored, or a formula) is this value |\n| `requires` | list of alias (at least one) | no | Fields that must be filled first, each drawn where the act stands (its section\'s fields, a block\'s, or the act\'s asks \u2014 never the header alone); a blocked act names each one missing |\n| `recommends` | list of alias (at least one) | no | Fields the act runs without and reads when filled: while one is empty, the act says what it will leave blank |\n| `asks` | list of (alias \\| [Act input](#act-input)) (at least one) | no | What the reader states when pressing it \u2014 fields of the record (written by the act) or inputs |\n| `set` | map of alias \u2192 (text \\| number \\| boolean \\| `null` \\| [Set formula](#set-formula)) | no | What the act writes: an option alias, a value, `"now"`, `"me"` or `"input:<name>"` \u2014 each the act\'s outcome, read-only elsewhere; or `{ "formula": \u2026 }`, computed over each acted row as it stands when the act runs and written then (a sell price at cost and markup): a starting value, which a person may change after and the act never recomputes |\n| `workflow` | alias | no | An authored workflow (src/workflows/<alias>.ts) the act runs after its checks, for what `set` cannot say |\n| `template` | alias | no | A document template the act makes a file from \u2014 an html one made a PDF, an excel one a workbook; over several rows, one file of them all, the rows listed under the entity\'s alias |\n| `templates` | list of [Act template](#act-template) (at least 2) | no | Several papers the act makes into `into` (a case\'s forms), instead of one `template`: drawn where the act stands as a list of its papers \u2014 each made one as its file, each not yet made as a placeholder naming it \u2014 the reader ticks which to make, and one press makes those, each replacing the paper its template made before |\n| `into` | alias | no | The files field of the one record the made document is kept in \u2014 remade, it replaces the paper its template made before, never a file a person put there |\n| `intake` | alias | no | A child the record lists in a rows block whose rows are its papers: the press takes papers, an agent reads them, and each is filed as a row of this child \u2014 its kind the child\'s title, its file in the child\'s files field |\n| `record` | [Act record](#act-record) | no | The press records a call, visit or meeting through the host \u2014 Stop ends it \u2014 and files it as a new row of a child the record lists: its audio, its screen where captured, its transcript |\n| `fills` | list of text (at least one) | no | With `intake`, what the papers read may fill: a field of the record, `<link>.<field>` on the row a required one-row link of the record names, or a child whose rows the papers add \u2014 shown before and after, and saved where it differs. With `record`, fields of the new row an agent fills from the transcript, saved as they come |\n| `confirm` | `true` | no | The press asks first; the question lists every active warning |\n| `danger` | `true` | no | The act destroys or cannot be undone |\n\n#### Act input\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `input` | alias | yes | A value the act asks for that is not a field of the record |\n| `label` | text | yes | What the reader is asked, in their words |\n| `type` | `"text"` \\| `"long_text"` \\| `"number"` \\| `"date"` \\| `"select"` \\| `"link"` \\| `"member"` | yes | What the reader states |\n| `options` | list of [Select option](#select-option) (at least one) | no | The choices a `select` input offers, one picked \u2014 written into a select holding the same option aliases |\n| `entity` | alias | no | The entity a `link` input picks one row of |\n| `required` | `true` | no | The act is refused without it |\n| `default` | alias | no | A field of the row whose value the input starts from, still changed at will |\n\n#### Act template\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `template` | alias | yes | A document template the act makes a paper of, kept in `into` \u2014 an html one made a PDF, an excel one a workbook |\n| `when` | map of alias \u2192 (list of alias (at least one) \\| boolean) | no | The paper starts ticked while each named select of the record holds one of these options and each named yes/no (stored, or a formula \u2014 how a multi-select is tested: `includes({needs}, {needs:x})`) is this value \u2014 a suggestion the reader changes, never a refusal; absent, it starts ticked |\n\n#### Set formula\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `formula` | text | yes | A formula over the acted row\'s fields, `{alias}` each, read as the entity\'s own formulas read them |\n\n#### Act record\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `into` | alias | yes | A child the record lists in a timeline or rows block, linking back to it by a single link: each recording is filed as a new row of it |\n| `audio` | alias | yes | The child\'s files field the recording\'s audio is kept in |\n| `transcript` | alias | yes | The child\'s text field the transcript is kept in, a speaker\'s turn per line |\n| `video` | alias | no | The child\'s files field the screen is kept in, where the recording captured it |\n| `at` | alias | no | The child\'s date field stamped with the moment the recording started |\n| `by` | alias | no | The child\'s member field set to the person who recorded |\n\n<!-- generated:end apps-acts -->\n\n<!-- generated:start rules-act -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `act.set-not-asked` | A field an act `set`s \u2014 an option, a value, `now`, `me`, an input \u2014 is the act\'s outcome: read-only everywhere else and never asked by an add (`create`); a required one takes a `default`. A field an act computes by a `formula` is a starting value, which a person may change after. |\n| `act.set-formula` | `set: { <field>: { formula } }` reads fields of the acted row and computes what the field holds \u2014 a number, a date, text or a yes/no, typed as the entity\'s own formulas are \u2014 never the status or a milestone; a bound a write rule holds the field to is checked on the computed value as the act runs. |\n| `act.of` | An act\'s `of` names a child the record lists in a rows block; it works on the one child row it is pressed on \u2014 or, `on: "rows"`, on the rows ticked together where the record lists that child as tasks \u2014 never on the rows in view. |\n| `act.writes-line-key` | An act never writes the link or the key naming an expected line under its record \u2014 a line\'s key is stated by filling it. |\n| `act.makes-or-runs` | An act makes a document (`template`, `templates`) or runs an authored `workflow`, never both \u2014 the workflow makes what it needs. |\n| `act.view` | An act over every row in view (`on: "view"`) writes (`set`), makes a document or runs a workflow, and states no `requires`. |\n| `act.requires-drawn` | Every field an act `requires` is drawn where the act stands \u2014 its section\'s fields, a block\'s, its asks, or an unstaged section\'s where its own is unstaged \u2014 never the header alone. |\n| `act.requires-written` | A field an act requires that only an act writes is written by an act offered together with it, or asked by the act itself. |\n| `act.recommends` | A field an act `recommends` is not also in its `requires`. |\n| `act.set` | `set` writes each field a value it holds \u2014 an option alias, a number, a yes/no, a text, `"now"` on a date, `"me"` on a member, `"input:<name>"` of an input the act asks of the same kind \u2014 or clears an optional one with `null`. |\n| `act.set-milestone` | An act stamps a milestone, never clears one \u2014 unticking clears it and every later one. |\n| `act.into` | `into` is a files field of the one record the act is pressed on, beside its `template` or `templates`. |\n| `act.templates` | `templates` names each template once, kept in `into`, on one record pressed alone \u2014 never beside `template`, `asks`, `of` or an `on` over several rows. |\n| `act.history-copy` | What a status move asks is copied onto the history\'s field of the same name, which holds the same kind of value. |\n| `act.intake` | `intake` names a child the record lists in a rows block that expects its title \u2014 a single select, the paper\'s kind \u2014 and is narrowed by no `where`, holding exactly one files field and needing nothing an added line does not fill; the act runs on one record, states `fills`, and states nothing but its `label`, `when` and `requires` beside them. |\n| `act.record` | `record` names a child the record draws in a timeline block or a rows block with no `expect`, `under` or `where`, linking back by one single link, whose row nothing requires beyond what the recording writes: `audio` and `video` its files fields, `transcript` a text field, `at` a date, `by` a member field, each a field of its own; the act runs on one record and states nothing but its `label`, `when`, `requires` and `fills` beside it. |\n| `act.fills` | `fills` names each once, only beside `intake` or `record`. Beside `record`, each is a field of the recording\'s row that the recording itself does not write. Beside `intake`: a field of the record, `<link>.<field>` on the row a required one-row link of the record names, or a child the record adds rows of in a rows block with no `expect`, `under` or `where` \u2014 its rows filled in the fields that block adds them with, among them every field an added row cannot be made without. Each field filled is one a person writes holding text, a number, a date, a yes/no or options, never a status, a milestone or a field an act writes. |\n| `act.fills-drawn` | *Noted, never refused:* A field `fills` writes that the record draws nowhere: the review shows the change, the record does not. |\n| `act.fills-new` | *Noted, never refused:* A child `fills` adds rows of, whose natural key the papers cannot state: every row read is added, so the same papers read twice list them twice. |\n| `act.input` | An input has a name of its own \u2014 never another ask\'s, a field\'s, or the record input its workflow takes \u2014 `options` exactly when it is a select, `entity` exactly when it is a link, and a `default` field holding what it states. |\n\n<!-- generated:end rules-act -->\n\nAn act whose `when` does not hold is not offered \u2014 each select named holding one of its\noptions, each yes/no named (stored, or a formula: a period that has ended) its `true` or\n`false`; every one that holds stands at the\nfoot of the section naming it in `acts` \u2014 a correction in that section heading\'s \u22EF, one\nof a child row on its row (a correction in the row\'s \u22EF). An act\'s `requires` are fields\nits section draws \u2014 its `fields`, a block\'s, the act\'s `asks`, or those of another section\nwith no `at` where its own has none; one shown only in the header or a staged section\nelsewhere is refused. One whose `requires` are\nempty is offered disabled, naming each; one whose `recommends` are empty names what\nit will leave blank and still presses; one a standing check blocks is offered\ndisabled in the check\'s words. An input\'s `default` is the row\'s field it starts from. The generated `act_<alias>` workflow re-checks all three on the\nserver, writes `set` and what was asked, and appends the status history where the\nact moves the status \u2014 each thing it asked copied onto the history row where the\nhistory has a field of the same name. `template` makes a document from the row and\nkeeps it in `into`; over several rows it makes one document of them all, the rows\nlisted under the entity\'s alias, and hands it back. `templates` instead names a set of\npapers kept in `into`, listed where the act stands \u2014 each made one as its file, each\nnot made yet as a placeholder \u2014 those whose `when` holds ticked to start; the press\nmakes the ticked ones (sent in `templates`), each replacing the file its template made\nbefore (the file whose `document_template_id` is that template \u2014 never a file a person\nput there), and the set downloads as one archive (`archive_<alias>`). A field an act writes is read-only everywhere else, never asked by an add \u2014 a required one takes a `default`. A `select` input offers its own options and is written into\na select holding the same option aliases; a `link` input picks one row of its `entity`\nand a `member` input one person, each written into a field of its kind. A field an act\nboth `requires` and `asks` never disables the press \u2014 the act asks it, required.\n\n`on: "view"` runs on every row the register shows \u2014 its search, status and filters \u2014\neach row checked before any is written. `of` names a rows block\'s child entity: the\nact is offered on each child row (its menu and its drawer), its `when`, `requires`,\n`set` and checks are the child\'s, it runs on that row, and a check of the record that\nlocks the child rows locks it too. With `on: "rows"` it runs on the child rows ticked\ntogether in the record\'s task list (\xA7 Tasks) \u2014 every row checked before any is written;\nit is refused where the record lists no tasks of that child.\n\n`intake` reads papers: the press takes files, an agent (`act_<alias>` among the app\'s\nagents) reads them, and each is filed as the record\'s line of its kind in the named\nchild \u2014 that child\'s title \u2014 or a new line, its file in the child\'s one files field.\nWhat they state of each field `fills` names is shown beside what the row holds; the save\nwrites only what differs \u2014 the record\'s fields, `<link>.<field>` on the row a required\none-row link names, a child\'s rows (one found by the child\'s `natural_key` changed, else\nadded) \u2014 each refused as its own save is, and nothing written while one refuses.\n\n```jsonc\n{ "alias": "read_papers", "label": "Read papers", "intake": "customer_paper", "fills": ["phone", "contact.email", "branch"] }\n```\n\n### Checks\n\n```jsonc\n"checks": [{ "field": "release_warning" }, { "field": "unpaid", "blocks": ["gate_out"] }, { "field": "locked", "blocks": ["edit"] }]\n```\n\n<!-- generated:start apps-checks -->\n\n#### Check\n\n| Key | Type | Required | Meaning |\n|---|---|---|---|\n| `field` | alias | yes | A yes/no or text formula of the record (or of the child `of` names): true or non-empty means the check stands; its label is its short name on a row\'s line, and a text formula\'s words are the sentence the record and a tooltip say |\n| `of` | alias | no | A rows block\'s child entity this check is a formula of: standing on a child row, it refuses what it blocks of that row |\n| `blocks` | `"all"` \\| list of (alias \\| `"edit"` \\| `"delete"`) (at least one) | no | Acts, "edit" (the saves and the Delete), "delete" (the Delete alone) or "all" that the standing check refuses; absent, it only warns |\n| `resolve` | alias | no | An act of this app, on the rows the check stands on, that clears it: the check\'s line links to the act where it stands, and the act keeps its own conditions |\n\n<!-- generated:end apps-checks -->\n\n<!-- generated:start rules-check -->\n\n#### Rules the check enforces\n\n| Rule | |\n|---|---|\n| `check.field` | A check\'s `field` is a yes/no or text formula of the record, or of the child `of` names. |\n| `check.of` | A check\'s `of` names a child the record lists in a rows block. |\n| `check.not-placed` | A check\'s field is never also placed on the record \u2014 the line under the header says it. |\n| `check.blocks` | A check of the record blocks its acts, `"edit"`, `"delete"` or `"all"`; a check of a child blocks that row\'s saves, its Delete, the acts `of` that child, or all \u2014 never the record\'s acts. |\n| `check.resolve` | `resolve` names an act of this app on the rows the check stands on, pressed on one row, not refused by the check, and drawn as a button \u2014 never a correction. |\n| `check.restates-meter` | A warning reading a gated meter\'s figure against its bound or pass mark is refused \u2014 the meter draws it. |\n\n<!-- generated:end rules-check -->\n\nA check is a yes/no or text formula of the record: it stands while true or\nnon-empty, and a text formula\'s words are what the reader sees. Without `blocks`\nit warns; with them it refuses the acts named, `"edit"` the record\'s own saves (and\nits Delete), `"delete"` its Delete alone, or `"all"` \u2014 on the server, in every write it\nnames. A check `of` a rows block\'s child stands on each child row: it refuses that\nrow\'s saves (`edit`), its Delete (`delete`), the acts `of` that child it names, or\n`all` of them \u2014 never the record\'s acts. A warning reading the figure of a meter that\n`gates` marks against its bound or its pass mark is refused: the meter already draws it.\n\nThe check field\'s `label` is its short name \u2014 "Over budget", "Release expired": a row\'s\nline says it in the check\'s tone (the most severe of several, and how many more), while\nits words whole are the row\'s tooltip and stand under the record\'s header. So label it\nas the reader names the problem, in a few words, and let a text formula say the\nsentence. The compiler records the fields a formula reads: a row drawing each of them\nas a control says nothing of the check on its line (the empty control says it), and a\ncheck with no `resolve` links to the first of them the record draws.\n\n## What a run remembers\n\nThe WORKSPACE remembers what each entity, field, select option, role and\ntemplate alias became here, plus which record each first row landed on and the\nfile each document path was uploaded as. The model file itself holds no live id\n\u2014 it is the portable half, and the same file applies to a demo workspace and to\na customer\'s \u2014 so the join lives where the things it names live, and one model\napplied to two workspaces holds two bindings that know nothing of each other.\n\nIt is the workspace\'s and not the file\'s because a model file is never\ncommitted: memory kept beside it is one author\'s disk, absent for a teammate, on\na second machine or after a delete \u2014 and every one of those goes quietly back to\nbinding by label, which is what grows the second table.\n\nEvery later apply binds through it: it takes the remembered id first and falls\nback to a label only for an alias nothing has bound \u2014 a field you have just\nadded, or a workspace nothing has applied this model to. That is what makes a\nrelabel on EITHER side a rename rather than one thing the workspace lacks and\none the model lacks: `apply` binds the thing it has always meant and reports the\nmove \u2014 which side is right is yours to decide, not a reason to refuse the run.\n\nA bound target the workspace no longer holds IS a refusal, by name: re-binding\nto whatever carries that label today is how the model comes to point at\nsomebody else\'s table. `lotics run restore_table` puts a deleted table back.\n\n`lotics model pull` writes a workspace that already works as a model file \u2014 the\nstarting point for another business\'s model, never a source of truth: it carries\none business\'s words and stops describing that workspace the moment either\nchanges.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "singular": "Customer",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n { "alias": "logo", "label": "Logo", "type": "files" },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "singular": "Order",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "format": "currency",\n "currency": "VND"\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "format": "currency", "currency": "VND" }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n },\n { "alias": "note", "label": "Note", "type": "text" }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "records": {\n "customer": { "title": "name", "image": "logo", "party": "organization", "figure": "total_ordered" },\n "order": { "title": "customer", "subtitle": ["code", "placed_on"], "figure": "amount", "starts": { "placed_on": "today" } }\n },\n "apps": [\n {\n "alias": "customers",\n "name": "Customers",\n "entity": "customer",\n "register": {\n "columns": ["tier"],\n "readings": [{ "breakdown": "customer", "by": "tier", "value": "total_ordered" }]\n },\n "record": {\n "sections": [\n { "title": "Account", "fields": ["tier"] },\n { "title": "Orders", "blocks": [{ "rows": "order", "columns": ["total"], "create": ["code", "amount"] }] }\n ]\n }\n },\n {\n "alias": "orders",\n "name": "Orders",\n "entity": "order",\n "register": {\n "filters": ["customer"],\n "create": ["code", "customer", "placed_on"],\n "readings": [{ "trend": "order", "over": "placed_on", "value": "amount" }]\n },\n "record": { "door": "drawer" }\n }\n ],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\nEach app of this file, as its reader will see it:\n\n```\nApp customers \u2014 "Customers" over Customers (customer)\n Register \u2014 table (default), newest first (default)\n row [Logo] \xB7 Name \xB7 figure Total ordered\n columns Tier\n filters \u2014\n readings "Tier" \u2014 Total ordered summed per Tier, over the rows in view (default)\n add Name (default)\n Record \u2014 a drawer (default)\n header image Logo \xB7 title Name \xB7 figure Total ordered\n checks \u2014\n header \u22EF Delete\n sections 1. "Account"\n fields Tier\n 2. "Orders"\n block Orders (default) \u2014 rows of Orders through Customer; columns Total with VAT; add asks Order no., Amount\n thread \u2014\n history \u2014\n acts \u2014\n Reads Customers, Orders\n\nApp orders \u2014 "Orders" over Orders (order)\n Register \u2014 table (default), newest first (default)\n row Customer \xB7 under it Order no., Placed on \xB7 figure Amount\n columns \u2014\n filters Customer\n readings "Amount" \u2014 Amount summed per period by Placed on, over the rows in view (default)\n add Order no., Customer, Placed on \u2014 starting Placed on at today\n Record \u2014 a drawer\n header title Customer \xB7 subtitle Order no., Placed on \xB7 figure Amount\n checks \u2014\n header \u22EF Delete\n sections 1. the record\'s details (default)\n fields Note\n thread \u2014\n history \u2014\n acts \u2014\n Reads Orders, Customers\n\n```\n\nEach `(default)` is a value the model left to the system: the door a record opens\nthrough \u2014 a drawer, since neither record has a stage or more than three sections \u2014 the\norder rows open in, what an add asks, and the order\'s one section \u2014 every field a\nperson writes that the header does not already show. The customer\'s orders\nare a block in its "Orders" section, never its own `Orders` link as a field as well:\none fact, one place. An order is titled by its customer and read by its number under\nit; the block names what its add asks, since a subtitle or a figure is never asked by\ndefault. `Total ordered` is a rollup, so it is read and never asked; the\norders app\'s record opens in a drawer, because its rows are worked one after another.\n';
38922
40146
 
38923
40147
  // AGENTS.md
38924
40148
  var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent needs before driving this CLI: which surface answers which question, what the\nconventions are, and where the traps are.\n\n| Read | For |\n|---|---|\n| `lotics --help` | The verb inventory (\xA7 COMMANDS) and global flags. The verb LIST is generated and never stale; the prose beside each verb is hand-written, so where it disagrees with `docs/cli_reference.md`, the reference wins. |\n| `lotics tools` \xB7 `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |\n| `lotics docs` \xB7 `lotics docs <area>[/<section>]` | This CLI's references, carried inside it so each describes the binary answering. Capped at a page: a doc that does not fit hands back its opening and the addresses into it, so the next call is smaller than the last. A custom-code app's SDK reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the app. |\n| `lotics docs model` \xB7 `lotics docs model/<section>` | How to write a `model.json` \u2014 the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Every top-level key, every field type with the config it needs, the row format, the rules, and a worked example; complete example models of several trades are listed at `https://lotics.ai/presets/index.json`. `lotics model apply` checks the file (every problem in one run), applies its tables and mints a version of each app; the WORKSPACE remembers what each alias became, so every later apply binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics model pull` goes the other way \u2014 the workspace's model, rebuilt from what owns each part. |\n| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE \u2014 clarify, model, apply or build, prove, look \u2014 for an app stated in a model and for a custom-code app. Read it once before starting an app. Looking is `lotics run screenshot_app`, then `lotics download` for each PNG. |\n| [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas \u2014 the detail `--help` compresses. |\n| [docs/data_model.md](./docs/data_model.md) | Tables, their fields, and how they relate \u2014 one fact per column, one entity per table and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied, a copy boundary that accounts for every source field, provenance as a link, a declared natural key, history as rows, and why derived DEPTH costs more than row count; then every field type, its properties, computed fields, and what `update_fields` takes. Separate from building_an_app because every workspace starts with tables and many never get an app. |\n| [docs/filters.md](./docs/filters.md) | The one filter grammar every filter-taking tool, view and rollup reads \u2014 conditions, groups, and the operators per field type. |\n| [docs/workflows.md](./docs/workflows.md) | Writing a workflow body \u2014 triggers, steps, what an expression reads, the helpers, approvals and agent steps, and table lifecycle workflows. |\n| [docs/app_bindings.md](./docs/app_bindings.md) | The queries, workflows and agents an app binds \u2014 the query tree, typed inputs and outputs, and an agent's declaration. |\n| [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates. |\n| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the workspace facts an agent can't guess; who can read a doc; catalog-then-stage retrieval. |\n| [docs/migration.md](./docs/migration.md) | What to DO when something an earlier CLI wrote to disk no longer matches what it does \u2014 local app projects are gone, and each verb that read one has a replacement. |\n| [README.md](./README.md) | Install, auth, and worked examples. |\n\n## Two surfaces, and the trap between them\n\n**`lotics tools` is not the CLI's capability list.** There are two disjoint surfaces:\n\n- **Tools** (`lotics tools`, `lotics run <tool>`) \u2014 the *agent tool registry*: what an agent, workflow,\n or automation may call. Workspace data, templates, knowledge, admin.\n- **Commands** (`lotics --help` \xA7 COMMANDS) \u2014 the CLI's *own verbs*: auth and org/workspace scoping,\n file upload and download, and the four that touch a local file for an app \u2014 `model apply`,\n `model pull`, `app create --custom` and `app deploy`.\n\nSeveral capabilities exist **only** as commands and appear nowhere in `lotics tools` \u2014 downloading a\nfile is the one most often mistaken for missing. Concluding \"the platform can't do X\" from the tool\nlist alone is a mistake; check both.\n\n**`lotics <verb> --help` prints that verb's entries from \xA7 COMMANDS** (`lotics model --help`,\n`lotics file download --help`); `lotics report --help` prints the report frame. To find out whether\nsomething exists, read `lotics --help` \xA7 COMMANDS \u2014 the whole section, not a narrow grep.\n\n## Conventions that hold across every command\n\n- **Scope is resolved per invocation.** `LOTICS_ORG` / `LOTICS_WORKSPACE` (or `LOTICS_API_KEY`) scope a\n single call without changing the active org or a directory pin \u2014 the safe way to touch one tenant\n from a shell serving many. Every command that resolves a workspace echoes its target to **stderr**\n (`lotics \u2192 <org> / <workspace>`); read it back before trusting a write. Resolution precedence is\n in README \xA7 Organizations.\n- **A machine with no key can still sign in, and the sign-in never blocks you.** `lotics auth\n login <email>` prints the page a person opens (also mailed) and the code that page must show,\n records the request, and EXITS. They press Confirm whenever they get to it; the next command that\n NEEDS a credential collects the key before doing its own work, so \"sign in\" costs you one command\n and then re-running what you wanted. Never wrap it in a timeout waiting for a human \u2014 `--wait`\n exists if you really want one blocking command, and killing that one is safe too (the request\n survives and the next command claims it). A command run before Confirm exits 1 naming the page and\n code again; once the 15 minutes are up it says to ask again. `lotics setup` falls into the same\n flow by itself when the email it was given already has an account: it stops having created\n nothing, and the SAME command run again carries on.\n- **A credential is either a SIGN-IN or an API KEY, and `logout` treats them differently.** A profile\n from `auth login` / `auth signup` acts as the person who confirmed it and is theirs \u2014 `lotics auth\n logout` revokes it server-side, and it lapses on its own after 90 idle days (each use pushes that\n out). A profile from `auth api-key` holds a key an ADMIN issued. A key created in Settings carries\n its OWN access \u2014 every app and table, or only the ones chosen on the key, so a listing that comes\n back short is the key's reach, not a bug \u2014 while a key created FOR a person carries that\n person's access and dies with their membership. Either is routinely also on a server and on\n other machines, so logout only forgets it locally and says so; only an admin revokes it. A profile that states no kind (saved before the field existed) is resolved against the SERVER\n and revoked only if the answer is a sign-in; a bare `--api-key` / `LOTICS_API_KEY` names no\n profile to remove at all. Nothing is ever revoked on a guess \u2014 between two, the destructive one is\n wrong. `lotics auth whoami` prints the kind, asking the server when the store cannot say.\n- **A key created in Settings never administers the organization, whatever its access.** The verbs\n `docs/cli_reference.md` marks *admin only* split in two under a key: `workspace doctor`, which only\n reads inside a workspace the key reaches, runs as before, while applying or pulling a model\n (`model apply`, `model pull`, `setup`), managing people, sharing or\n ownership, creating or deleting a workspace, changing workspace settings, setting credit limits,\n reading the access log and publishing an app's API answer `403` and name the remedy: an admin signed in, so\n `lotics auth login <email>` and run it again. A sign-in acts as that person and is refused none of\n them. Do not retry a `403` with the same credential and do not ask for a wider key \u2014 no answer on\n the key's own screen grants this.\n- **A 401 names its remedy \u2014 act on the hint, do not retry.** \"This credential expired / was\n revoked / belongs to a member who is no longer active\" carries the one remedy that ends this\n credential: run `lotics auth login <email>` for a sign-in, or ask the admin who issued it for a\n new key. A credential minted before that was recorded carries BOTH, because nothing on the row\n tells them apart \u2014 so on a box with no browser, take the second. Only an UNRECOGNIZED key gets\n the generic \"Invalid or disabled API key\", and that one is generic on purpose, so re-sending it\n teaches nothing. A `reason` rides on the body for a script to branch on, since the code stays\n `unauthorized` for every 401.\n- **Large payloads bypass `ARG_MAX`** \u2014 `lotics run <tool> @args.json`, or piped stdin behind `-`. A leading `@` is\n unambiguously a file path (JSON args start with `{`).\n- **stdout is the payload, stderr is the narration.** Progress, status lines, and the target echo go to\n stderr; the result goes to stdout, so piping stays clean. `--json` switches stdout from the\n agent-readable summary to the full structured object.\n- **Every tool is invoked one way \u2014 `lotics run <tool>`.** Including the ones that RUN something\n (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app\n (`set_app_query`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command\n exists only for work that touches a local file.\n- **Every change to an app mints a version of it, and rolling back is the undo.** An apply, a\n deploy, a single `set_app_*` or `remove_app_*`: each is a new version, and `rollback_app` makes an\n earlier one current again. It restores the app \u2014 never a table change or a row a workflow wrote,\n so try a write on a throwaway record.\n- **Exit codes are assertable, and they report the WORK rather than the call.** `lotics run` exits\n non-zero when a `run_app_workflow` or `run_app_agent` result's own envelope carries a failed\n `status` (`error`/`failed`/`cancelled`) \u2014 any other tool's top-level status is data and exits 0 \u2014\n so `lotics run \u2026 && next-step` cannot walk past a refused run; `workspace doctor` exits non-zero on\n findings. An unrecognized status exits 0 \u2014 the list is an allowlist of failure, so a status added\n later never turns a working script red \u2014 and a parked run (`awaiting_input`) is not a failure.\n- **An app that PUBLISHES an API turns every later binding write into a release.** Publishing\n snapshots what the app's queries, workflows and agents promise to callers outside it \u2014 a\n customer's own site or server, which nobody here can redeploy. From then on an additive change\n re-snapshots silently and a breaking one is REFUSED, naming each change; the tool's\n `acknowledge_breaking_api_change` carries it out and snapshots the break as a new contract version.\n- **Exposure is per app, all or nothing** \u2014 a public share or a key reaches every alias an app\n declares, so what outsiders may call is a second app over the same tables\n ([docs/building_an_app.md](./docs/building_an_app.md) \xA7 7).\n- **`--print-created` / `--cleanup` on any call that reports `side_effects`.** The first prints the\n records created plus a paste-ready cleanup plan and what cannot be auto-undone; the second runs\n those deletes (records only \u2014 never files, integrations or notifications). Neither is a rollback.\n- **Text output is the default and is built for reading**; reach for `--json` only when a field is\n needed programmatically.\n- **`lotics report '<json>'` is the channel for what nothing else records.** Reach for it\n when the platform is genuinely missing something (a capability that does not exist \u2014 no\n command ran), when a success was wrong (exited 0, wrong effect), or when an error did\n not name the remedy \u2014 not for your own mistakes, which the logs already show.\n **It is a frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, because a log\n reconstructs what you RAN and never what you WANTED, and that gap is the report. `goal` and\n `actual` are required; there is no severity or category to pick. The\n session's commands attach themselves \u2014 do not retype them. Run it bare for the full prompt; a\n long one rides `@file.json` or an explicit `-` for stdin (bare NEVER reads stdin).\n- **`LOTICS_TELEMETRY=1` correlates a whole session** so the authoring loop's rough edges can be\n found and fixed. Off by default; set it in the shell profile, not per command (each invocation is\n its own process). It sends no arguments, no file contents, and no record data \u2014 see README\n \xA7 Diagnostics.\n\n## Where this CLI is not the answer\n\n- **Editing an app in a local directory.** An app stated in a model is changed by applying the\n model again; its bindings through their `set_app_*` tools; a custom-code app's code by deploying\n its directory again. Nothing reads a local copy of a live app back.\n- **OAuth connections.** Attaching a connected account is web-only; the CLI can list them.\n";
@@ -40314,11 +41538,11 @@ function parseCreatedRecords(value) {
40314
41538
  const groups = [];
40315
41539
  for (const entry of value) {
40316
41540
  if (!entry || typeof entry !== "object") continue;
40317
- const tableId = entry.table_id;
41541
+ const tableId2 = entry.table_id;
40318
41542
  const recordIds = entry.record_ids;
40319
- if (typeof tableId !== "string" || !Array.isArray(recordIds)) continue;
41543
+ if (typeof tableId2 !== "string" || !Array.isArray(recordIds)) continue;
40320
41544
  const ids = recordIds.filter((id) => typeof id === "string");
40321
- if (ids.length > 0) groups.push({ table_id: tableId, record_ids: ids });
41545
+ if (ids.length > 0) groups.push({ table_id: tableId2, record_ids: ids });
40322
41546
  }
40323
41547
  return groups;
40324
41548
  }