@lotics/cli 0.245.0 → 0.247.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.
package/AGENTS.md CHANGED
@@ -8,14 +8,13 @@ conventions are, and where the traps are.
8
8
  | `lotics --help` | The verb inventory (§ 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. |
9
9
  | `lotics tools` · `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |
10
10
  | `lotics docs` · `lotics docs <area>[/<section>]` | Every reference the packages installed beside the project actually ship — `@lotics/app-runtime`, `@lotics/ui` and the document engines each carry their own, and this index only covers THIS package. Discovered by looking, not by a list, so it reports the installed VERSION of each: a doc always describes the code that is really there. 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. |
11
- | `lotics docs model` · `lotics docs model/<section>` | How to write a `model.json` — the file a workspace is built from, and the apps planned over it. Carried inside this CLI at its own version, since it describes this CLI's plan checker, and paged like every doc: the first page is the working order and the section addresses. Two forms: `{"from": "<preset-slug>", "variants", "rename", "entities", "rows", "apply"}`, which names a preset by slug and carries only what this business differs by, and the full one, spelled out, for when no preset is the trade: every top-level key, every field type with the config it needs, the row format, the rules, and a worked example. Offline. It also covers the `apply` list — published packages copied in after the model's own tables, each with an optional `bind` onto them — and the `preset` block a published model carries. `lotics scaffold check` then proves the file — offline, except for the one read a `from` file's preset needs — including every branch of a preset merged onto its base; `lotics setup` applies it and refuses a table name the workspace already has, `lotics scaffold apply` adopts that table and adds what is missing, and the WORKSPACE remembers what each alias became, so every later run binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics scaffold export` goes the other way — a workspace printed as one of these files, to edit into another business's model; a starting point, never a source of truth. |
11
+ | `lotics docs model` · `lotics docs model/<section>` | How to write a `model.json` — the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Carried inside this CLI at its own version, since it describes this CLI's model checker, and paged like every doc: the first page is the working order and the section addresses. Two forms: `{"from": "<preset-slug>", "variants", "rename", "entities", "rows", "apply"}`, which names a preset by slug and carries only what this business differs by, and the full one, spelled out, for when no preset is the trade: every top-level key, every field type with the config it needs, the row format, the rules, and a worked example. Offline. It also covers the `apply` list — published packages copied in after the model's own tables, each with an optional `bind` onto them — and the `preset` block a published model carries. `lotics scaffold check` then proves the file — offline, except for the one read a `from` file's preset needs — including every branch of a preset merged onto its base; `lotics setup` applies it and refuses a table name the workspace already has, `lotics scaffold apply` adopts that table and adds what is missing, and the WORKSPACE remembers what each alias became, so every later run binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics scaffold export` goes the other way — a workspace printed as one of these files, to edit into another business's model; a starting point, never a source of truth. |
12
12
  | [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE — scaffold, model, types, queries, workflows, screens, ship — and the deploy-free inner loop. The other references describe contracts; this one is the order they go in and why. Read it once before starting an app. |
13
- | [docs/clauses.md](./docs/clauses.md) | Every clause a plan's app or screen can state, what it draws, and a plan fragment stating it — generated from the schema, so it lists no clause the schema lacks and misses none it has. |
14
13
  | [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas — the detail `--help` compresses. Read it before hand-building a `set_app_*` payload: several tools REPLACE rather than patch, and a CLI verb already owns the safe assembly. |
15
14
  | [docs/data_model.md](./docs/data_model.md) | How tables RELATE — one entity per table and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied between tables, a copy boundary that accounts for every source field, provenance as a link rather than a flag, a declared natural key so find-or-create never compares rendered text, and why derived DEPTH costs more than row count. Separate from building_an_app because every workspace starts with tables and many never get an app. The within-table half (one fact, one column) is stated at `create_table` / `update_table`, where you meet it while deciding. |
16
15
  | [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates. |
17
16
  | [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. |
18
- | [docs/migration.md](./docs/migration.md) | What to DO when a release changes the shape of a project this CLI owns. Read once, when something already on disk no longer matches what the CLI writes — for one, an app built from a plan is `app.json` rendered by `@lotics/app-runtime`, not a tree of TSX. |
17
+ | [docs/migration.md](./docs/migration.md) | What to DO when a release changes the shape of a project this CLI owns. Read once, when something already on disk no longer matches what the CLI writes — for one, an app built from a model is `app.json` version 2, rendered by `@lotics/app-runtime`. |
19
18
  | [README.md](./README.md) | Install, auth, and worked examples. |
20
19
 
21
20
  ## Two surfaces, and the trap between them
package/README.md CHANGED
@@ -31,8 +31,6 @@ package (reachable at `node_modules/@lotics/cli/docs/*.md` once installed):
31
31
  - [`docs/building_an_app.md`](docs/building_an_app.md) — building a custom-code app end to end:
32
32
  the sequence the steps go in (clarify → model → types → queries → workflows → screens → ship)
33
33
  and the deploy-free inner loop. Read it once before starting an app.
34
- - [`docs/clauses.md`](docs/clauses.md) — every clause a plan's app or screen can state, what it
35
- draws, and a plan fragment stating it, generated from the schema.
36
34
  - [`docs/document_templates.md`](docs/document_templates.md) — generate finished documents
37
35
  (PDF, Excel, Word, email) by filling reusable templates: the five template types, the
38
36
  create → generate → chain lifecycle, and the marker capabilities.
@@ -70,8 +68,8 @@ selects each.
70
68
 
71
69
  ## Or describe your own workspace
72
70
 
73
- A `model.json` names the tables, fields, options, links, views, roles and first rows, and
74
- `lotics setup` builds them. **Start from a preset when one is your trade** — name it instead of
71
+ A `model.json` names the tables, fields, options, links, views, roles and first rows, how a row
72
+ of each table is recognised, and the apps over them; `lotics setup` builds the tables. **Start from a preset when one is your trade** — name it instead of
75
73
  restating it:
76
74
 
77
75
  ```json
@@ -347,20 +345,21 @@ lotics knowledge rm kdc_... # archive
347
345
  ## Custom-code apps
348
346
 
349
347
  ```bash
350
- # SEE THE APP BEFORE IT EXISTS. The plan is bound against the workspace the model
351
- # WOULD become and every read is answered from the model's own `rows`, so there is
352
- # no app row, no table and no credential in the run — then the same headless walk
353
- # `app check --screens` takes, at the same two widths, measured by the same probes.
354
- # Exits 1 on any finding and prints what each phase cost. A model with no `rows` is
355
- # refused: a register over nothing measures clean. It renders in the cached project
356
- # of its dependency set (`~/.lotics/render/`, shared with `app check --screens`),
357
- # installed once from the registry — the kit and the runtime are published packages
358
- # — and rewritten from the plan on every run.
348
+ # SEE THE APP BEFORE IT EXISTS. The model's app is compiled against the workspace the
349
+ # model WOULD become and every read is answered from the model's own `rows` (a few
350
+ # synthesized for an entity that states none), so there is no app row, no table and
351
+ # no credential in the run — then the same headless walk `app check --screens` takes,
352
+ # at the same two widths, measured by the same probes: the register, the record in
353
+ # its door, the add. Exits 1 on any finding and prints what each phase cost. It
354
+ # renders in the cached project of its dependency set (`~/.lotics/render/`, shared
355
+ # with `app check --screens`), installed once from the registry — the kit and the
356
+ # runtime are published packages — and rewritten from the model on every run.
359
357
  lotics app preview model.json#sales_desk
360
358
  lotics app preview model.json#sales_desk --shots ./shots --width 375
361
359
 
362
360
  # Scaffold / pull / deploy a Vite+React+TS app project
363
361
  lotics app create "Sales Desk" # scaffold + deploy v1
362
+ lotics app create "Sales Desk" --from model.json#sales_desk # the model's app, as app.json
364
363
  lotics app create "Orders API" --api # no screens: its declarations are the whole surface,
365
364
  # so nothing is built and nothing is deployed
366
365
  lotics app pull app_... # bootstrap an existing app locally (incl. .lotics/*)
@@ -421,6 +421,15 @@ var __loticsProbe = (() => {
421
421
  };
422
422
  }
423
423
  var SINGLE_LINE_FIELDS = /* @__PURE__ */ new Set(["text", "search", "tel", "url", "email", "number"]);
424
+ function clippingAncestor(el, stop) {
425
+ for (let at = el.parentElement; at !== null; at = at.parentElement) {
426
+ const style = getComputedStyle(at);
427
+ const x = style.overflowX || style.overflow;
428
+ if (x === "hidden" || x === "clip") return at;
429
+ if (at === stop) return null;
430
+ }
431
+ return null;
432
+ }
424
433
  function truncatedLeaves(root = document.body) {
425
434
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
426
435
  const clippedAway2 = /^inset\(\s*50%/;
@@ -438,7 +447,16 @@ var __loticsProbe = (() => {
438
447
  if (box.width <= 1 && box.height <= 1) continue;
439
448
  if (clippedAway2.test(style.getPropertyValue("clip-path"))) continue;
440
449
  total += 1;
441
- if (style.overflow === "visible") continue;
450
+ if (style.overflow === "visible") {
451
+ const within2 = el.closest('.lotics-fact-groups, [role="listbox"]');
452
+ const clip = within2 === null ? null : clippingAncestor(el, within2);
453
+ if (clip !== null) {
454
+ const edge = clip.getBoundingClientRect();
455
+ const shown = Math.min(box.right, edge.right) - Math.max(box.left, edge.left);
456
+ if (box.right > edge.right + 1 || box.left < edge.left - 1) cut.push({ text: text.slice(0, 40), axis: "width", has: Math.max(0, Math.round(shown)), needs: Math.round(box.width) });
457
+ }
458
+ continue;
459
+ }
442
460
  const states = (property) => {
443
461
  const value = style.getPropertyValue(property);
444
462
  return value !== "" && value !== "none";
@@ -459,7 +477,7 @@ var __loticsProbe = (() => {
459
477
  const walk = (node) => {
460
478
  if (node.nodeType === Node.TEXT_NODE) {
461
479
  const parent = node.parentElement;
462
- if (!parent || skipTag.test(parent.tagName) || parent.closest("[data-content]") !== null) return;
480
+ if (!parent || skipTag.test(parent.tagName) || parent.tagName === "TEXTAREA" || parent.closest('[data-content], [contenteditable="true"]') !== null) return;
463
481
  const text = node.textContent || "";
464
482
  if (!text.trim()) return;
465
483
  total += 1;
@@ -694,8 +712,9 @@ var __loticsProbe = (() => {
694
712
  }
695
713
 
696
714
  // src/probe_collectors/anatomy.ts
697
- var BODY = ".lotics-record-page__body";
698
- var BRIEF = ".lotics-record-page__brief";
715
+ var FRAME = ".lotics-record-frame";
716
+ var HEADER = ".lotics-record-header";
717
+ var BLOCK = ".lotics-record-frame__block";
699
718
  var PRIMARY = '.lotics-button[data-variant="primary"]';
700
719
  var FIRST_VIEWPORTS = [
701
720
  { width: 1280, height: 800 },
@@ -705,11 +724,10 @@ var __loticsProbe = (() => {
705
724
  return FIRST_VIEWPORTS.reduce((near, one) => Math.abs(one.width - width) < Math.abs(near.width - width) ? one : near);
706
725
  }
707
726
  function surfaces(root) {
708
- return within(root, BODY).filter(onScreen).flatMap((body) => {
709
- const head = body.closest(".lotics-drawer__popup, .lotics-record-page");
710
- if (head === null) return [];
711
- const title = within(head, "h1, h2").find((one) => !body.contains(one) && onScreen(one));
712
- return [{ body, root: head, name: words2(title) || nameOf(head) || "(unnamed record)" }];
727
+ return within(root, FRAME).filter(onScreen).map((frame) => {
728
+ const head = frame.closest(".lotics-drawer__popup") ?? frame;
729
+ const title = within(head, `${HEADER}__title`).find(onScreen);
730
+ return { frame, root: head, name: words2(title) || nameOf(head) || "(unnamed record)" };
713
731
  });
714
732
  }
715
733
  function words2(el) {
@@ -720,44 +738,51 @@ var __loticsProbe = (() => {
720
738
  function headingPrimaries(root = document.body) {
721
739
  const drawn = surfaces(root);
722
740
  const loud = drawn.flatMap(
723
- (surface) => within(surface.body, PRIMARY).filter((act) => onScreen(act) && act.closest(".lotics-section-heading") !== null).map((act) => ({ record: surface.name, act: nameOf(act) || words2(act) }))
741
+ (surface) => within(surface.frame, PRIMARY).filter(
742
+ (act) => onScreen(act) && act.closest(BLOCK) !== null && (act.closest(`${FRAME}__action`) !== null || act.closest(".lotics-section-heading") !== null)
743
+ ).map((act) => ({ record: surface.name, act: nameOf(act) || words2(act) }))
724
744
  );
725
745
  return { total: drawn.length, loud };
726
746
  }
727
- function briefsBelowFold(root = document.body, width = window.innerWidth) {
747
+ function actsBelowFold(root = document.body, width = window.innerWidth) {
728
748
  const fold = firstViewport(width).height;
729
- const briefs = surfaces(root).flatMap((surface) => {
730
- const brief = within(surface.body, BRIEF).find(onScreen);
731
- return brief === void 0 ? [] : [{ record: surface.name, bottom: Math.round(brief.getBoundingClientRect().bottom) }];
749
+ const acts = surfaces(root).flatMap((surface) => {
750
+ const seat = within(surface.frame, `${FRAME}__act`).find(onScreen);
751
+ const button = seat === void 0 ? void 0 : within(seat, ".lotics-button").find(onScreen);
752
+ return button === void 0 ? [] : [{ record: surface.name, act: nameOf(button) || words2(button), bottom: Math.round(button.getBoundingClientRect().bottom) }];
732
753
  });
733
- return { total: briefs.length, fold, below: briefs.filter((one) => one.bottom > fold) };
754
+ return { total: acts.length, fold, below: acts.filter((one) => one.bottom > fold) };
734
755
  }
735
- function briefRepeats(root = document.body) {
756
+ function factRepeats(root = document.body) {
736
757
  let total = 0;
737
758
  const repeated = surfaces(root).flatMap((surface) => {
738
- const brief = within(surface.body, BRIEF).find(onScreen);
739
- if (brief === void 0) return [];
759
+ const header = within(surface.root, HEADER).find(onScreen);
760
+ if (header === void 0) return [];
740
761
  const head = new Set(
741
- within(surface.root, "*").filter((el) => el.childElementCount === 0 && !surface.body.contains(el) && el.closest('button, [role="button"], [role="tab"]') === null && onScreen(el)).map((el) => said(words2(el))).filter((text) => text.length > 1)
762
+ within(header, "*").filter((el) => el.childElementCount === 0 && el.closest('button, [role="button"], [role="menu"]') === null && onScreen(el)).map((el) => said(words2(el))).filter((text) => text.length > 1)
742
763
  );
743
- const values = within(brief, ".lotics-record-facts__value").filter(onScreen);
744
- total += values.length;
745
- return values.flatMap((value) => {
746
- const text = said(words2(value));
747
- return text !== "" && head.has(text) ? [{ record: surface.name, value: words2(value), where: "head" }] : [];
764
+ const fields = within(surface.frame, ".lotics-fact-groups__field").filter(onScreen);
765
+ total += fields.length;
766
+ return fields.flatMap((field) => {
767
+ const label = within(field, ".lotics-form-field__label")[0];
768
+ const value = within(field, "*").filter((el) => el.childElementCount === 0 && (label === void 0 || !label.contains(el)) && onScreen(el)).map((el) => words2(el)).join(" ").trim();
769
+ return value !== "" && head.has(said(value)) ? [{ record: surface.name, field: words2(label), value }] : [];
748
770
  });
749
771
  });
750
772
  return { total, repeated };
751
773
  }
752
- function emptyTabs(root = document.body) {
774
+ function emptyBlocks(root = document.body) {
753
775
  let total = 0;
754
776
  const empty = surfaces(root).flatMap((surface) => {
755
- const tabs = within(surface.body, '[role="tab"]').filter(onScreen);
756
- total += tabs.length;
757
- const open = tabs.find((tab) => tab.getAttribute("aria-selected") === "true");
758
- const panel = within(surface.body, '[role="tabpanel"]').find(onScreen);
759
- const bare = open !== void 0 && panel !== void 0 && within(panel, ".lotics-section").filter(onScreen).length === 0;
760
- return bare ? [{ record: surface.name, tab: words2(open).replace(/\d+$/, "").trim() }] : [];
777
+ const blocks = within(surface.frame, BLOCK).filter(onScreen);
778
+ total += blocks.length;
779
+ return blocks.flatMap((block) => {
780
+ const body = Array.from(block.children).filter(
781
+ (child) => !child.matches(`.lotics-section-heading, ${FRAME}__strip`) && onScreen(child)
782
+ );
783
+ const drawn = body.some((child) => words2(child) !== "" || child.querySelector("img, svg, input, textarea, button") !== null);
784
+ return drawn ? [] : [{ record: surface.name, block: block.getAttribute("aria-label") ?? "" }];
785
+ });
761
786
  });
762
787
  return { total, empty };
763
788
  }
@@ -768,7 +793,7 @@ var __loticsProbe = (() => {
768
793
  function clutterCensus(root = document.body) {
769
794
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
770
795
  const chrome = 'nav, [role="banner"]';
771
- const recordSurface = '.lotics-record-page, [role="dialog"]';
796
+ const recordSurface = '.lotics-record-frame, [role="dialog"]:not([data-side])';
772
797
  const headings = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
773
798
  const noteFloor = 3;
774
799
  const formFloor = 3;
@@ -778,7 +803,7 @@ var __loticsProbe = (() => {
778
803
  const sameEdge = 8;
779
804
  const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
780
805
  const surfaces2 = within(root, recordSurface).filter(onScreen);
781
- const called2 = (el) => el.matches(headings) ? words5(el) || "the section" : el.matches(recordSurface) ? words5(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record" : "the screen";
806
+ const called2 = (el) => el.matches(headings) ? words5(el) || "the section" : el.matches(recordSurface) ? words5(el.querySelector(".lotics-record-header__title, h1, h2")) || "the record" : "the screen";
782
807
  const rules = [];
783
808
  const primary = '[data-variant="primary"]';
784
809
  const acts = within(root, primary).filter(
@@ -884,34 +909,6 @@ var __loticsProbe = (() => {
884
909
  count: twice,
885
910
  offenders: repeated
886
911
  });
887
- const absent = /^[—–-]$/;
888
- let facts = 0;
889
- let standing = 0;
890
- const open = [];
891
- for (const surface of surfaces2) {
892
- for (const block of within(surface, ".lotics-record-facts")) {
893
- const fold = block.querySelector(".lotics-record-facts__more");
894
- if (fold === null) continue;
895
- for (const band of Array.from(block.querySelectorAll(".lotics-record-facts__group"))) {
896
- if ((fold.compareDocumentPosition(band) & Node.DOCUMENT_POSITION_FOLLOWING) === 0) continue;
897
- for (const cell of Array.from(band.querySelectorAll(".lotics-record-facts__fact"))) {
898
- const value = cell.querySelector(".lotics-record-facts__value");
899
- if (value === null || !onScreen(cell)) continue;
900
- facts += 1;
901
- if (!absent.test((value.textContent || "").trim())) continue;
902
- standing += 1;
903
- if (open.length < 3) open.push(`"${words5(cell.querySelector(".lotics-record-facts__label")) || words5(cell)}" is unstated and drawn`);
904
- }
905
- }
906
- }
907
- }
908
- rules.push({
909
- law: "hierarchy.md",
910
- rule: "How a record derives them",
911
- total: facts,
912
- count: standing,
913
- offenders: open
914
- });
915
912
  let boxes = 0;
916
913
  let overReserved = 0;
917
914
  const reserved = [];
@@ -1497,9 +1494,15 @@ var __loticsProbe = (() => {
1497
1494
  );
1498
1495
  return Math.max(0, width - drawn);
1499
1496
  }
1500
- function slotGap(register) {
1501
- const boxes = register.slots.map((slot) => slot.cells[0]).filter((cell) => cell !== void 0).map((cell) => cell.getBoundingClientRect()).sort((a, b) => a.left - b.left);
1502
- return boxes.length < 2 ? 0 : Math.max(0, Math.round(boxes[1].left - boxes[0].right));
1497
+ function slotGaps(register) {
1498
+ const first = register.slots.map((slot) => slot.cells[0]).filter((cell) => cell !== void 0);
1499
+ const boxes = first.map((cell) => cell.getBoundingClientRect()).sort((a, b) => a.left - b.left);
1500
+ if (boxes.length < 2) return { gap: 0, beyond: 0 };
1501
+ const drawn = boxes.slice(1).map((box, index) => Math.max(0, box.left - boxes[index].right));
1502
+ const parent = first[0].parentElement;
1503
+ const declared = parent === null ? Number.NaN : Number.parseFloat(getComputedStyle(parent).columnGap);
1504
+ const gap = Number.isFinite(declared) ? declared : Math.min(...drawn);
1505
+ return { gap: Math.round(gap), beyond: drawn.reduce((sum, one) => sum + Math.max(0, one - gap), 0) };
1503
1506
  }
1504
1507
  function shedWithRoom(root = document.body) {
1505
1508
  let total = 0;
@@ -1508,8 +1511,8 @@ var __loticsProbe = (() => {
1508
1511
  const shed = shedOf(register.root);
1509
1512
  if (shed.length === 0 || register.rows.length === 0) continue;
1510
1513
  total += shed.length;
1511
- const unused = Math.round(register.slots.reduce((sum, slot) => sum + spare(slot), 0));
1512
- const gap = slotGap(register);
1514
+ const { gap, beyond } = slotGaps(register);
1515
+ const unused = Math.round(register.slots.reduce((sum, slot) => sum + spare(slot), 0) + beyond);
1513
1516
  for (const column of shed) if (column.width + gap <= unused) found.push({ ...column, gap, unused });
1514
1517
  }
1515
1518
  return { total, shed: found };
@@ -1573,39 +1576,10 @@ var __loticsProbe = (() => {
1573
1576
 
1574
1577
  // src/probe_collectors/right_form.ts
1575
1578
  var DAY_RUN_PART = ".lotics-timeline__dayhead";
1576
- var SIDE = 'aside, [role="complementary"], nav, [role="navigation"]';
1577
- var IN_PAGE = 'button, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], a[href^="#"]';
1578
- var HEADING3 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
1579
- var RECORD_ROWS = 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="list"], ul, ol';
1580
- var PARKED = [
1581
- {
1582
- is: 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"], [role="slider"], [role="checkbox"], [role="radio"], [role="switch"], form, [role="form"]',
1583
- called: "a field"
1584
- },
1585
- { is: 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="listbox"]', called: "a list of rows" },
1586
- { is: 'article, [role="article"], [role="feed"], [role="log"]', called: "a conversation" },
1587
- { is: 'img, svg, picture, canvas, [role="img"], [role="figure"]', called: "a picture" },
1588
- { is: "a[href]", called: "a link out of the page" }
1589
- ];
1590
1579
  function rightFormCensus(root = document.body) {
1591
1580
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
1592
- const recordSurface = '.lotics-record-page, [role="dialog"]';
1593
1581
  const columnFloor = 3;
1594
- const twoTrackWidth = 1016;
1595
- const trackHeight = 240;
1596
1582
  const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
1597
- const surfaces2 = within(root, recordSurface).filter(onScreen);
1598
- const called2 = (el) => words5(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record";
1599
- const readingInside = (side) => {
1600
- for (const kind of PARKED) {
1601
- for (const el of Array.from(side.querySelectorAll(kind.is))) {
1602
- if (!onScreen(el) || el.closest(IN_PAGE) !== null) continue;
1603
- const named = nameOf(el).slice(0, 40) || words5(el);
1604
- return named === "" ? kind.called : `${kind.called} ("${named}")`;
1605
- }
1606
- }
1607
- return null;
1608
- };
1609
1583
  const rules = [];
1610
1584
  const choosers = within(root, 'select, [role="listbox"]').filter(onScreen);
1611
1585
  let twoOption = 0;
@@ -1624,7 +1598,7 @@ var __loticsProbe = (() => {
1624
1598
  }
1625
1599
  rules.push({
1626
1600
  law: "data_entry.md",
1627
- rule: "Choosing a CHOICE control \u2014 `choiceControl`, never taste",
1601
+ rule: "Choosing a CHOICE control \u2014 by the set, never taste",
1628
1602
  total: choosers.length,
1629
1603
  count: twoOption,
1630
1604
  offenders: pairs
@@ -1701,105 +1675,12 @@ var __loticsProbe = (() => {
1701
1675
  }
1702
1676
  }
1703
1677
  rules.push({
1704
- law: "templates.md",
1678
+ law: "screen.md",
1705
1679
  rule: "Which device answers the question \u2192 Read what happened, or what is planned, in time",
1706
1680
  total: columns.length,
1707
1681
  count: grouped,
1708
1682
  offenders: runs
1709
1683
  });
1710
- const namesItself = (region) => {
1711
- const heading = Array.from(region.querySelectorAll(HEADING3)).find((el) => el.closest("summary") === null);
1712
- return heading !== void 0 || region.querySelector(RECORD_ROWS) !== null;
1713
- };
1714
- let folds = 0;
1715
- let sections = 0;
1716
- const hidden = [];
1717
- for (const surface of surfaces2) {
1718
- for (const fold of Array.from(surface.querySelectorAll(".lotics-accordion__trigger, details, [aria-expanded]"))) {
1719
- if (!onScreen(fold) || fold.closest('[role="complementary"]') !== null) continue;
1720
- if (fold.hasAttribute("aria-haspopup") || fold.matches(HOLDS_A_VALUE)) continue;
1721
- folds += 1;
1722
- const opened = fold.tagName === "DETAILS" ? [fold] : pointedAt(fold, "aria-controls");
1723
- if (!fold.matches(".lotics-accordion__trigger") && !opened.some(namesItself)) continue;
1724
- sections += 1;
1725
- if (hidden.length < 3) {
1726
- const named = nameOf(fold).slice(0, 40);
1727
- hidden.push(`${called2(surface)}: "${named === "" ? "(unnamed)" : named}" is a section behind a fold`);
1728
- }
1729
- }
1730
- }
1731
- rules.push({
1732
- law: "templates.md",
1733
- rule: "Which device answers the question \u2192 See what NAMES this record",
1734
- total: folds,
1735
- count: sections,
1736
- offenders: hidden
1737
- });
1738
- let read = 0;
1739
- let split = 0;
1740
- const tracks = [];
1741
- if (window.innerWidth >= twoTrackWidth) {
1742
- for (const surface of surfaces2) {
1743
- read += 1;
1744
- const box = surface.getBoundingClientRect();
1745
- const owners = [surface, ...Array.from(surface.querySelectorAll("*"))];
1746
- let found = null;
1747
- for (const owner of owners) {
1748
- const columnsHere = Array.from(owner.children).filter((child) => {
1749
- if (!onScreen(child)) return false;
1750
- if (child.matches(SIDE) || child.querySelector(SIDE) !== null) return false;
1751
- const rect = child.getBoundingClientRect();
1752
- return rect.height >= trackHeight && rect.height >= box.height * 0.5 && rect.width >= box.width * 0.2;
1753
- });
1754
- for (let a = 0; a < columnsHere.length && found === null; a++) {
1755
- for (let b = a + 1; b < columnsHere.length && found === null; b++) {
1756
- const left = columnsHere[a].getBoundingClientRect();
1757
- const right = columnsHere[b].getBoundingClientRect();
1758
- const apart = left.right <= right.left + 8 || right.right <= left.left + 8;
1759
- const overlap = Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top);
1760
- const together = Math.max(left.right, right.right) - Math.min(left.left, right.left);
1761
- if (apart && overlap >= Math.min(left.height, right.height) * 0.5 && together >= box.width * 0.7) {
1762
- found = [columnsHere[a], columnsHere[b]];
1763
- }
1764
- }
1765
- }
1766
- if (found !== null) break;
1767
- }
1768
- if (found !== null) {
1769
- split += 1;
1770
- if (tracks.length < 3) {
1771
- const left = Math.round(found[0].getBoundingClientRect().width);
1772
- const right = Math.round(found[1].getBoundingClientRect().width);
1773
- tracks.push(`${called2(surface)}: two columns side by side, ${left}px and ${right}px`);
1774
- }
1775
- continue;
1776
- }
1777
- for (const side of Array.from(surface.querySelectorAll(SIDE))) {
1778
- if (!onScreen(side)) continue;
1779
- const rect = side.getBoundingClientRect();
1780
- if (rect.width > box.width * 0.6) continue;
1781
- const column = Array.from(surface.querySelectorAll("*")).some((other) => {
1782
- if (other === side || side.contains(other) || other.contains(side) || !onScreen(other)) return false;
1783
- const beside = other.getBoundingClientRect();
1784
- const apart = beside.left >= rect.right - 8 || beside.right <= rect.left + 8;
1785
- const overlap = Math.min(beside.bottom, rect.bottom) - Math.max(beside.top, rect.top);
1786
- return apart && beside.width >= box.width * 0.4 && overlap >= rect.height * 0.5;
1787
- });
1788
- if (!column) continue;
1789
- const parked = readingInside(side);
1790
- if (parked === null) continue;
1791
- split += 1;
1792
- if (tracks.length < 3) tracks.push(`${called2(surface)}: ${parked} beside the reading column`);
1793
- }
1794
- }
1795
- }
1796
- rules.push({
1797
- law: "hierarchy.md",
1798
- rule: "Depth \u2014 four surfaces, four jobs",
1799
- total: read,
1800
- count: split,
1801
- offenders: tracks
1802
- });
1803
1684
  return rules;
1804
1685
  }
1805
1686
 
@@ -1977,22 +1858,22 @@ var __loticsProbe = (() => {
1977
1858
  record_primary: {
1978
1859
  law: "hierarchy.md",
1979
1860
  section: "One primary act per surface",
1980
- fix: "Draw a section's add secondary (`SectionHeadingAdd`): the record's one primary act is its head's \u2014 the next queued act, else the step its stage leads with."
1861
+ fix: "Draw a block's add secondary: the record's one primary act is the act open now, in its `ActPanel`."
1981
1862
  },
1982
- brief_fold: {
1863
+ act_fold: {
1983
1864
  law: "hierarchy.md",
1984
- section: "Depth \u2014 four surfaces, four jobs",
1985
- fix: "Keep in the brief only what the head's act needs \u2014 the newest entry, the words to say, the one band placed first \u2014 and move the rest to a tab, until its foot is in the first viewport unscrolled."
1865
+ section: "Budgets a reviewer can count",
1866
+ fix: "Seat the open act where the frame pins it \u2014 `RecordFrame`'s `act`, never a button of the page's own \u2014 so it stays in view while the facts run on."
1986
1867
  },
1987
- brief_repeats: {
1868
+ fact_repeats: {
1988
1869
  law: "hierarchy.md",
1989
1870
  section: "One fact, one place",
1990
- fix: "Take the value out of the brief's band: the head already states it, so the band read under it is the same fact twice."
1871
+ fix: "Take the field out of the facts: the header already states it \u2014 its title, its status, the line under it or its figure."
1991
1872
  },
1992
- empty_tab: {
1873
+ empty_block: {
1993
1874
  law: "hierarchy.md",
1994
- section: "Depth \u2014 four surfaces, four jobs",
1995
- fix: "Draw no tab over nothing: a tab is drawn only over a part with a body, so a part with no rows passes none."
1875
+ section: "Whitespace is structure",
1876
+ fix: "Draw the block's body, or its empty line: a block under a heading that draws nothing is a press that leads nowhere."
1996
1877
  },
1997
1878
  render: {
1998
1879
  law: "reviewing.md",
@@ -2018,7 +1899,7 @@ var __loticsProbe = (() => {
2018
1899
  fix: "Move a fact down a tier, fold it, or take it off the screen \u2014 a budget failing is a tiering question, never a permission to shrink something."
2019
1900
  },
2020
1901
  right_form: {
2021
- law: "templates.md",
1902
+ law: "screen.md",
2022
1903
  section: "Which device answers the question",
2023
1904
  fix: "Draw what the JOB names: the device index gives the entry and the wrong device beside it, and a shape is picked by what the reader must do rather than by what the data is."
2024
1905
  },
@@ -2224,37 +2105,37 @@ var __loticsProbe = (() => {
2224
2105
  ...signature("record_primary"),
2225
2106
  total: primaries.total,
2226
2107
  count: primaries.loud.length,
2227
- detail: primaries.loud.slice(0, 5).map((one) => `"${one.record}": "${one.act}" is primary in a section's heading row, beside the head's act`)
2108
+ detail: primaries.loud.slice(0, 5).map((one) => `"${one.record}": "${one.act}" is primary in a block's heading row, beside the record's open act`)
2228
2109
  });
2229
2110
  }
2230
- const folded = briefsBelowFold(surface);
2111
+ const folded = actsBelowFold(surface);
2231
2112
  if (folded.below.length > 0) {
2232
2113
  findings.push({
2233
- probe: "brief_fold",
2234
- ...signature("brief_fold"),
2114
+ probe: "act_fold",
2115
+ ...signature("act_fold"),
2235
2116
  total: folded.total,
2236
2117
  count: folded.below.length,
2237
- detail: folded.below.slice(0, 5).map((one) => `"${one.record}": the brief ends at ${one.bottom}px, past the ${folded.fold}px first viewport`)
2118
+ detail: folded.below.slice(0, 5).map((one) => `"${one.record}": "${one.act}" ends at ${one.bottom}px, past the ${folded.fold}px first viewport`)
2238
2119
  });
2239
2120
  }
2240
- const repeats = briefRepeats(surface);
2121
+ const repeats = factRepeats(surface);
2241
2122
  if (repeats.repeated.length > 0) {
2242
2123
  findings.push({
2243
- probe: "brief_repeats",
2244
- ...signature("brief_repeats"),
2124
+ probe: "fact_repeats",
2125
+ ...signature("fact_repeats"),
2245
2126
  total: repeats.total,
2246
2127
  count: repeats.repeated.length,
2247
- detail: repeats.repeated.slice(0, 5).map((one) => `"${one.record}": the brief states "${one.value}", which the head already says`)
2128
+ detail: repeats.repeated.slice(0, 5).map((one) => `"${one.record}": the fact "${one.field}" reads "${one.value}", which the header already says`)
2248
2129
  });
2249
2130
  }
2250
- const tabs = emptyTabs(surface);
2251
- if (tabs.empty.length > 0) {
2131
+ const blocks = emptyBlocks(surface);
2132
+ if (blocks.empty.length > 0) {
2252
2133
  findings.push({
2253
- probe: "empty_tab",
2254
- ...signature("empty_tab"),
2255
- total: tabs.total,
2256
- count: tabs.empty.length,
2257
- detail: tabs.empty.slice(0, 5).map((one) => `"${one.record}": the tab "${one.tab}" holds nothing`)
2134
+ probe: "empty_block",
2135
+ ...signature("empty_block"),
2136
+ total: blocks.total,
2137
+ count: blocks.empty.length,
2138
+ detail: blocks.empty.slice(0, 5).map((one) => `"${one.record}": the block "${one.block}" draws nothing`)
2258
2139
  });
2259
2140
  }
2260
2141
  findings.push(...censusFindings("clutter", clutterCensus(surface)));
@@ -2339,8 +2220,8 @@ var __loticsProbe = (() => {
2339
2220
  function recordLinks() {
2340
2221
  const here = location.pathname + location.search;
2341
2222
  const doors2 = Array.from(
2342
- document.querySelectorAll('a[href], [data-opens="page"], button, [role="button"], [role="link"]')
2343
- ).filter((door) => door.matches('a[href], [data-opens="page"]') || isEmptyDoor(door));
2223
+ document.querySelectorAll('a[href], [data-opens="page"], [data-opens="event"], button, [role="button"], [role="link"]')
2224
+ ).filter((door) => door.matches('a[href], [data-opens="page"], [data-opens="event"]') || isEmptyDoor(door));
2344
2225
  return doors2.filter((door) => {
2345
2226
  if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
2346
2227
  if (doors2.some((other) => other !== door && other.contains(door))) return false;
@@ -2440,6 +2321,9 @@ var __loticsProbe = (() => {
2440
2321
  const folds = Array.from(document.querySelectorAll('[aria-expanded="false"]')).filter(
2441
2322
  (el) => reachable(el) && el.getAttribute("aria-haspopup") === null
2442
2323
  );
2324
+ const tabs = Array.from(document.querySelectorAll('[role="tab"][aria-selected="false"]')).filter(
2325
+ (el) => onScreen(el) && el.closest('nav, [role="banner"]') === null && !open.some((overlay) => overlay.contains(el))
2326
+ );
2443
2327
  const acts = Array.from(
2444
2328
  document.querySelectorAll('[data-variant="primary"], [data-variant="secondary"]')
2445
2329
  ).filter((el) => {
@@ -2462,7 +2346,8 @@ var __loticsProbe = (() => {
2462
2346
  ...menus.slice(0, 1).map((el) => ({ kind: "menu", el })),
2463
2347
  ...folds.slice(0, 1).map((el) => ({ kind: "fold", el })),
2464
2348
  ...acts.map((el) => ({ kind: "dialog", el })),
2465
- ...parties.map((el) => ({ kind: "party", el }))
2349
+ ...parties.map((el) => ({ kind: "party", el })),
2350
+ ...tabs.map((el) => ({ kind: "tab", el }))
2466
2351
  ];
2467
2352
  }
2468
2353
  function doors() {