machinalayout 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,6 +1,15 @@
1
+ import {
2
+ createViewportMatrix,
3
+ defineMachinaScreens,
4
+ defineMachinaViewports,
5
+ expandScreenViewportTasks,
6
+ getMachinaViewport,
7
+ slugMachinaArtifactName
8
+ } from "./chunk-33CKBEJH.js";
1
9
  import {
2
10
  MachinaReactView
3
- } from "./chunk-HU6XYOH7.js";
11
+ } from "./chunk-ZVDE7PX4.js";
12
+ import "./chunk-2ZQ2RFFI.js";
4
13
  import "./chunk-RJYRJ3LD.js";
5
14
  import {
6
15
  MachinaTextView
@@ -10,9 +19,11 @@ import {
10
19
  parseMachinaTextInline
11
20
  } from "./chunk-BJOQRPPX.js";
12
21
  import {
13
- MachinaLayoutError,
14
22
  toResolvedTree
15
- } from "./chunk-TR24ERZT.js";
23
+ } from "./chunk-SVWYWI7I.js";
24
+ import {
25
+ MachinaLayoutError
26
+ } from "./chunk-VREK57S3.js";
16
27
 
17
28
  // src/validation.ts
18
29
  function assertFiniteNumber(value, fieldName) {
@@ -947,6 +958,149 @@ function formatRect(rect) {
947
958
  return `x=${rect.x} y=${rect.y} w=${rect.width} h=${rect.height}`;
948
959
  }
949
960
 
961
+ // src/stackGeometry.ts
962
+ function copyRect(rect) {
963
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
964
+ }
965
+ function applyPadding(parentRect, padding) {
966
+ return {
967
+ x: parentRect.x + padding.left,
968
+ y: parentRect.y + padding.top,
969
+ width: parentRect.width - padding.left - padding.right,
970
+ height: parentRect.height - padding.top - padding.bottom
971
+ };
972
+ }
973
+ function assertContentNonNegative(contentRect, code) {
974
+ if (contentRect.width < 0 || contentRect.height < 0) {
975
+ throw new MachinaLayoutError(
976
+ code,
977
+ `${code === "StackContentNegative" ? "stack" : "grid"} content size cannot be negative after applying padding`
978
+ );
979
+ }
980
+ }
981
+ function requireNode(layout, nodeId) {
982
+ const node = layout.nodes[nodeId];
983
+ if (!node) throw new MachinaLayoutError("InvalidId", `node id not found: ${nodeId}`);
984
+ return node;
985
+ }
986
+ function requireStackArrange(layout, parentId) {
987
+ const parent = requireNode(layout, parentId);
988
+ if (parent.arrange?.kind !== "stack") {
989
+ throw new MachinaLayoutError(
990
+ "ExpectedStackArrange",
991
+ `expected stack arrange for node: ${parentId}`
992
+ );
993
+ }
994
+ return parent.arrange;
995
+ }
996
+ function getArrangeContentRect(parentRect, arrange) {
997
+ if (!arrange) return copyRect(parentRect);
998
+ if (arrange.kind === "stack") {
999
+ const contentRect = applyPadding(parentRect, normalizePadding(arrange.padding));
1000
+ assertContentNonNegative(contentRect, "StackContentNegative");
1001
+ return contentRect;
1002
+ }
1003
+ if (arrange.kind === "grid") {
1004
+ const contentRect = applyPadding(parentRect, normalizePadding(arrange.padding));
1005
+ assertContentNonNegative(contentRect, "GridContentNegative");
1006
+ return contentRect;
1007
+ }
1008
+ return copyRect(parentRect);
1009
+ }
1010
+ function getStackContentRect(layout, parentId) {
1011
+ const parent = requireNode(layout, parentId);
1012
+ const arrange = requireStackArrange(layout, parentId);
1013
+ return getArrangeContentRect(parent.rect, arrange);
1014
+ }
1015
+ function getStackMainAxisMetrics(layout, parentId) {
1016
+ const parent = requireNode(layout, parentId);
1017
+ const arrange = requireStackArrange(layout, parentId);
1018
+ const contentRect = getArrangeContentRect(parent.rect, arrange);
1019
+ const isHorizontal = arrange.axis === "horizontal";
1020
+ const childIds = [...layout.children[parentId] ?? []];
1021
+ const childMetrics = childIds.map((id) => {
1022
+ const child = requireNode(layout, id);
1023
+ const rect = copyRect(child.rect);
1024
+ const mainStart = isHorizontal ? rect.x - contentRect.x : rect.y - contentRect.y;
1025
+ const mainSize = isHorizontal ? rect.width : rect.height;
1026
+ const crossStart = isHorizontal ? rect.y - contentRect.y : rect.x - contentRect.x;
1027
+ const crossSize = isHorizontal ? rect.height : rect.width;
1028
+ return {
1029
+ id,
1030
+ rect,
1031
+ mainStart,
1032
+ mainEnd: mainStart + mainSize,
1033
+ mainSize,
1034
+ crossStart,
1035
+ crossEnd: crossStart + crossSize,
1036
+ crossSize,
1037
+ frameKind: child.frame.kind,
1038
+ z: child.z,
1039
+ layer: child.layer
1040
+ };
1041
+ });
1042
+ const contentMainSize = isHorizontal ? contentRect.width : contentRect.height;
1043
+ const contentCrossSize = isHorizontal ? contentRect.height : contentRect.width;
1044
+ const totalChildMainSize = childMetrics.reduce((sum, metric) => sum + metric.mainSize, 0);
1045
+ const totalGapSize = (arrange.gap ?? 0) * Math.max(0, childMetrics.length - 1);
1046
+ const usedMainSize = totalChildMainSize + totalGapSize;
1047
+ return {
1048
+ parentId,
1049
+ axis: arrange.axis,
1050
+ parentRect: copyRect(parent.rect),
1051
+ contentRect,
1052
+ padding: normalizePadding(arrange.padding),
1053
+ gap: arrange.gap ?? 0,
1054
+ childIds,
1055
+ childMetrics,
1056
+ contentMainSize,
1057
+ contentCrossSize,
1058
+ totalChildMainSize,
1059
+ totalGapSize,
1060
+ usedMainSize,
1061
+ unusedMainSize: contentMainSize - usedMainSize
1062
+ };
1063
+ }
1064
+ function getStackChildRects(layout, parentId) {
1065
+ requireStackArrange(layout, parentId);
1066
+ const rects = {};
1067
+ for (const childId of layout.children[parentId] ?? []) {
1068
+ rects[childId] = copyRect(requireNode(layout, childId).rect);
1069
+ }
1070
+ return rects;
1071
+ }
1072
+ function getRemainingStackRect(layout, options) {
1073
+ const metrics = getStackMainAxisMetrics(layout, options.parentId);
1074
+ const byId = new Map(metrics.childMetrics.map((metric) => [metric.id, metric]));
1075
+ const after = options.afterChildren ?? [];
1076
+ const before = options.beforeChildren ?? [];
1077
+ const start = after.length === 0 ? 0 : Math.max(...after.map((id) => requireStackMetric(byId, id).mainEnd));
1078
+ const end = before.length === 0 ? metrics.contentMainSize : Math.min(...before.map((id) => requireStackMetric(byId, id).mainStart));
1079
+ const size = end - start;
1080
+ if (size < 0) {
1081
+ throw new MachinaLayoutError(
1082
+ "StackQueryInvalidRange",
1083
+ `remaining stack interval is negative for parent: ${options.parentId}`
1084
+ );
1085
+ }
1086
+ return metrics.axis === "horizontal" ? {
1087
+ x: metrics.contentRect.x + start,
1088
+ y: metrics.contentRect.y,
1089
+ width: size,
1090
+ height: metrics.contentRect.height
1091
+ } : {
1092
+ x: metrics.contentRect.x,
1093
+ y: metrics.contentRect.y + start,
1094
+ width: metrics.contentRect.width,
1095
+ height: size
1096
+ };
1097
+ }
1098
+ function requireStackMetric(metrics, childId) {
1099
+ const metric = metrics.get(childId);
1100
+ if (!metric) throw new MachinaLayoutError("InvalidId", `stack child id not found: ${childId}`);
1101
+ return metric;
1102
+ }
1103
+
950
1104
  // src/lerp.ts
951
1105
  function assertFiniteNumber2(value) {
952
1106
  if (!Number.isFinite(value)) {
@@ -1056,8 +1210,18 @@ export {
1056
1210
  assertNonNegativePadding,
1057
1211
  assertNonNegativeSize,
1058
1212
  compileLayoutRows,
1213
+ createViewportMatrix,
1214
+ defineMachinaScreens,
1215
+ defineMachinaViewports,
1216
+ expandScreenViewportTasks,
1059
1217
  flattenResolvedTree,
1060
1218
  formatRect,
1219
+ getArrangeContentRect,
1220
+ getMachinaViewport,
1221
+ getRemainingStackRect,
1222
+ getStackChildRects,
1223
+ getStackContentRect,
1224
+ getStackMainAxisMetrics,
1061
1225
  lerpNumber,
1062
1226
  lerpRect,
1063
1227
  lerpResolvedLayouts,
@@ -1069,5 +1233,6 @@ export {
1069
1233
  resolveLayoutRows,
1070
1234
  resolveUiLength,
1071
1235
  selectLayoutRowsForRoot,
1236
+ slugMachinaArtifactName,
1072
1237
  toResolvedTree
1073
1238
  };
@@ -0,0 +1,8 @@
1
+ import { S as SummarizeMachinaDomOptions, M as MachinaDomSummary } from '../types-DLYAhNXw.js';
2
+ export { a as MachinaDomSummaryNode } from '../types-DLYAhNXw.js';
3
+ import '../types-B90jb3RW.js';
4
+
5
+ type DomRoot = ParentNode | Element | Document;
6
+ declare function summarizeMachinaDom(rootOrOptions?: DomRoot | SummarizeMachinaDomOptions): MachinaDomSummary;
7
+
8
+ export { MachinaDomSummary, SummarizeMachinaDomOptions, summarizeMachinaDom };
@@ -0,0 +1,97 @@
1
+ // src/inspect/summarizeMachinaDom.ts
2
+ var DEFAULT_SELECTOR = "[data-machina-node-id]";
3
+ var DEFAULT_MAX_TEXT_LENGTH = 120;
4
+ function getGlobalDocument() {
5
+ return typeof document === "undefined" ? void 0 : document;
6
+ }
7
+ function isOptions(value) {
8
+ if (value === void 0 || value === null || typeof value !== "object") return false;
9
+ return "root" in value || "selector" in value || "includeTextExcerpt" in value || "includeA11y" in value || "generatedAt" in value || "maxTextLength" in value || "includeEmptyNodes" in value;
10
+ }
11
+ function readOptionalAttribute(element, name) {
12
+ const value = element.getAttribute(name);
13
+ return value === null || value === "" ? void 0 : value;
14
+ }
15
+ function rectFromElement(element) {
16
+ const rect = element.getBoundingClientRect();
17
+ return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
18
+ }
19
+ function textExcerpt(element, maxLength) {
20
+ const normalized = (element.textContent ?? "").replace(/\s+/g, " ").trim();
21
+ if (normalized === "") return void 0;
22
+ return normalized.length > maxLength ? normalized.slice(0, maxLength) : normalized;
23
+ }
24
+ function canMatchRoot(root) {
25
+ return typeof root.matches === "function";
26
+ }
27
+ function queryMatchingElements(root, selector) {
28
+ const matches = Array.from(root.querySelectorAll(selector));
29
+ if (canMatchRoot(root) && root.matches(selector)) return [root, ...matches];
30
+ return matches;
31
+ }
32
+ function nearestMatchingAncestor(element, selected, root) {
33
+ let parent = element.parentElement;
34
+ while (parent) {
35
+ if (selected.has(parent)) return parent;
36
+ if (parent === root) return void 0;
37
+ parent = parent.parentElement;
38
+ }
39
+ return void 0;
40
+ }
41
+ function makeSummaryNode(element, options) {
42
+ const node = {
43
+ tagName: element.tagName.toLowerCase(),
44
+ rect: rectFromElement(element),
45
+ children: []
46
+ };
47
+ const nodeId = readOptionalAttribute(element, "data-machina-node-id");
48
+ const view = readOptionalAttribute(element, "data-machina-view");
49
+ const slot = readOptionalAttribute(element, "data-machina-slot");
50
+ const debugLabel = readOptionalAttribute(element, "data-machina-debug-label");
51
+ const layer = readOptionalAttribute(element, "data-machina-layer");
52
+ if (nodeId !== void 0) node.nodeId = nodeId;
53
+ if (view !== void 0) node.view = view;
54
+ if (slot !== void 0) node.slot = slot;
55
+ if (debugLabel !== void 0) node.debugLabel = debugLabel;
56
+ if (layer !== void 0) node.layer = layer;
57
+ if (options.includeA11y) {
58
+ const role = readOptionalAttribute(element, "role");
59
+ const ariaLabel = readOptionalAttribute(element, "aria-label");
60
+ if (role !== void 0) node.role = role;
61
+ if (ariaLabel !== void 0) node.ariaLabel = ariaLabel;
62
+ }
63
+ if (options.includeTextExcerpt) {
64
+ const excerpt = textExcerpt(element, options.maxTextLength);
65
+ if (excerpt !== void 0) node.textExcerpt = excerpt;
66
+ }
67
+ return node;
68
+ }
69
+ function summarizeMachinaDom(rootOrOptions) {
70
+ const options = isOptions(rootOrOptions) ? rootOrOptions : { root: rootOrOptions };
71
+ const selector = options.selector ?? DEFAULT_SELECTOR;
72
+ const root = options.root ?? getGlobalDocument();
73
+ const summary = { schemaVersion: 1, rootSelector: selector, nodes: [] };
74
+ if (options.generatedAt !== void 0) summary.generatedAt = options.generatedAt;
75
+ if (root === void 0 || typeof root.querySelectorAll !== "function") return summary;
76
+ const elements = queryMatchingElements(root, selector);
77
+ const selected = new Set(elements);
78
+ const nodeByElement = /* @__PURE__ */ new Map();
79
+ const nodeOptions = {
80
+ includeTextExcerpt: options.includeTextExcerpt ?? false,
81
+ includeA11y: options.includeA11y ?? false,
82
+ maxTextLength: options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH
83
+ };
84
+ for (const element of elements) nodeByElement.set(element, makeSummaryNode(element, nodeOptions));
85
+ for (const element of elements) {
86
+ const node = nodeByElement.get(element);
87
+ if (!node) continue;
88
+ const parent = nearestMatchingAncestor(element, selected, root);
89
+ const parentNode = parent === void 0 ? void 0 : nodeByElement.get(parent);
90
+ if (parentNode) parentNode.children.push(node);
91
+ else summary.nodes.push(node);
92
+ }
93
+ return summary;
94
+ }
95
+ export {
96
+ summarizeMachinaDom
97
+ };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- import { b as ResolvedLayoutDocument, N as NodeId, R as Rect, a as ResolvedLayoutNode } from '../types-BudfpzZX.js';
2
+ import { o as MachinaDebugOverlayMode } from '../debugOverlay-pJpj0n5H.js';
3
+ import { b as ResolvedLayoutDocument, N as NodeId, R as Rect, a as ResolvedLayoutNode } from '../types-B90jb3RW.js';
3
4
 
4
5
  type MachinaSlotProps<TViewData = unknown, TNodeData = unknown> = {
5
6
  id: NodeId;
@@ -13,6 +14,12 @@ type MachinaSlotProps<TViewData = unknown, TNodeData = unknown> = {
13
14
  type MachinaRenderLayer = {
14
15
  z: number;
15
16
  };
17
+ type MachinaReactDebugOverlayOptions = {
18
+ mode?: MachinaDebugOverlayMode;
19
+ labels?: boolean;
20
+ borders?: boolean;
21
+ selectedNodeId?: string;
22
+ };
16
23
  type MachinaReactViewProps = {
17
24
  layout: ResolvedLayoutDocument;
18
25
  views?: Record<string, React.ComponentType<MachinaSlotProps>>;
@@ -27,7 +34,8 @@ type MachinaReactViewProps = {
27
34
  nodeContainIntrinsicSize?: string;
28
35
  layers?: Record<string, MachinaRenderLayer>;
29
36
  defaultLayer?: string;
37
+ debugOverlay?: MachinaReactDebugOverlayOptions;
30
38
  };
31
39
  declare function MachinaReactView(props: MachinaReactViewProps): React.JSX.Element;
32
40
 
33
- export { MachinaReactView, type MachinaReactViewProps, type MachinaSlotProps };
41
+ export { type MachinaReactDebugOverlayOptions, MachinaReactView, type MachinaReactViewProps, type MachinaSlotProps };
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  MachinaReactView
3
- } from "../chunk-HU6XYOH7.js";
4
- import "../chunk-TR24ERZT.js";
3
+ } from "../chunk-ZVDE7PX4.js";
4
+ import "../chunk-2ZQ2RFFI.js";
5
+ import "../chunk-SVWYWI7I.js";
6
+ import "../chunk-VREK57S3.js";
5
7
  export {
6
8
  MachinaReactView
7
9
  };
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { StyleProp, ViewStyle } from 'react-native';
3
- import { N as NodeId, R as Rect, a as ResolvedLayoutNode, b as ResolvedLayoutDocument } from '../types-BudfpzZX.js';
3
+ import { N as NodeId, R as Rect, a as ResolvedLayoutNode, b as ResolvedLayoutDocument } from '../types-B90jb3RW.js';
4
4
 
5
5
  type MachinaNativeSlotProps<TViewData = unknown, TNodeData = unknown> = {
6
6
  id: NodeId;
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  toResolvedTree
3
- } from "../chunk-TR24ERZT.js";
3
+ } from "../chunk-SVWYWI7I.js";
4
+ import "../chunk-VREK57S3.js";
4
5
 
5
6
  // src/react-native/MachinaReactNativeView.tsx
6
7
  import React from "react";
@@ -0,0 +1,46 @@
1
+ type MachinaViewport = {
2
+ key: string;
3
+ width: number;
4
+ height: number;
5
+ deviceScaleFactor?: number;
6
+ label?: string;
7
+ tags?: readonly string[];
8
+ };
9
+ type MachinaViewportMatrix = readonly MachinaViewport[];
10
+ type MachinaScreen = {
11
+ key: string;
12
+ route: string;
13
+ fixture?: string;
14
+ viewports?: readonly string[];
15
+ tags?: readonly string[];
16
+ title?: string;
17
+ metadata?: Record<string, unknown>;
18
+ };
19
+ type MachinaScreenCatalog = {
20
+ screens: Record<string, MachinaScreen>;
21
+ order: string[];
22
+ };
23
+ type MachinaScreenViewportTask = {
24
+ key: string;
25
+ screenKey: string;
26
+ viewportKey: string;
27
+ route: string;
28
+ fixture?: string;
29
+ viewport: MachinaViewport;
30
+ screen: MachinaScreen;
31
+ tags: readonly string[];
32
+ artifactBaseName: string;
33
+ };
34
+ type ExpandOptions = {
35
+ screenKeys?: readonly string[];
36
+ viewportKeys?: readonly string[];
37
+ tags?: readonly string[];
38
+ };
39
+ declare function defineMachinaViewports(viewports: readonly MachinaViewport[]): MachinaViewportMatrix;
40
+ declare function createViewportMatrix(preset?: "standard-responsive" | "desktop-only" | "mobile-first"): MachinaViewportMatrix;
41
+ declare function defineMachinaScreens(screens: readonly MachinaScreen[]): MachinaScreenCatalog;
42
+ declare function slugMachinaArtifactName(input: string): string;
43
+ declare function getMachinaViewport(viewports: MachinaViewportMatrix, key: string): MachinaViewport;
44
+ declare function expandScreenViewportTasks(catalog: MachinaScreenCatalog, viewports: MachinaViewportMatrix, options?: ExpandOptions): MachinaScreenViewportTask[];
45
+
46
+ export { type MachinaViewport as M, type MachinaScreenViewportTask as a, type MachinaScreen as b, type MachinaScreenCatalog as c, type MachinaViewportMatrix as d, createViewportMatrix as e, defineMachinaScreens as f, defineMachinaViewports as g, expandScreenViewportTasks as h, getMachinaViewport as i, slugMachinaArtifactName as s };
@@ -181,4 +181,4 @@ type ResolvedLayoutTree = {
181
181
  children: ResolvedLayoutTree[];
182
182
  };
183
183
 
184
- export type { AbsoluteFrame as A, CellFrame as C, EdgeInsets as E, FrameSpec as F, GridArrange as G, LayoutRow as L, NodeId as N, OffsetSpec as O, Rect as R, StackAlign as S, UiLength as U, ResolvedLayoutNode as a, ResolvedLayoutDocument as b, LayoutDocument as c, ResolvedLayoutTree as d, AnchorFrame as e, ArrangeSpec as f, EdgeRef as g, FillFrame as h, FixedFrame as i, GridTrack as j, GuideFrame as k, GuideLength as l, LayerName as m, LayoutNode as n, LayoutRowVariant as o, LayoutVariantCondition as p, RectEdge as q, RootFrame as r, StackArrange as s, StackAxis as t, StackJustify as u };
184
+ export type { ArrangeSpec as A, CellFrame as C, EdgeInsets as E, FrameSpec as F, GridArrange as G, LayoutRow as L, NodeId as N, OffsetSpec as O, Rect as R, StackAxis as S, UiLength as U, ResolvedLayoutNode as a, ResolvedLayoutDocument as b, LayoutDocument as c, ResolvedLayoutTree as d, LayerName as e, AbsoluteFrame as f, AnchorFrame as g, EdgeRef as h, FillFrame as i, FixedFrame as j, GridTrack as k, GuideFrame as l, GuideLength as m, LayoutNode as n, LayoutRowVariant as o, LayoutVariantCondition as p, RectEdge as q, RootFrame as r, StackAlign as s, StackArrange as t, StackJustify as u };
@@ -0,0 +1,32 @@
1
+ import { R as Rect } from './types-B90jb3RW.js';
2
+
3
+ type MachinaDomSummaryNode = {
4
+ nodeId?: string;
5
+ view?: string;
6
+ slot?: string;
7
+ debugLabel?: string;
8
+ layer?: string;
9
+ tagName: string;
10
+ role?: string;
11
+ ariaLabel?: string;
12
+ textExcerpt?: string;
13
+ rect: Rect;
14
+ children: MachinaDomSummaryNode[];
15
+ };
16
+ type MachinaDomSummary = {
17
+ schemaVersion: 1;
18
+ rootSelector?: string;
19
+ generatedAt?: string;
20
+ nodes: MachinaDomSummaryNode[];
21
+ };
22
+ type SummarizeMachinaDomOptions = {
23
+ root?: ParentNode | Element | Document;
24
+ selector?: string;
25
+ includeTextExcerpt?: boolean;
26
+ includeA11y?: boolean;
27
+ maxTextLength?: number;
28
+ includeEmptyNodes?: boolean;
29
+ generatedAt?: string;
30
+ };
31
+
32
+ export type { MachinaDomSummary as M, SummarizeMachinaDomOptions as S, MachinaDomSummaryNode as a };
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
2
  import { PropType, Component, StyleValue } from 'vue';
3
- import { N as NodeId, R as Rect, a as ResolvedLayoutNode, b as ResolvedLayoutDocument } from '../types-BudfpzZX.js';
3
+ import { N as NodeId, R as Rect, a as ResolvedLayoutNode, b as ResolvedLayoutDocument } from '../types-B90jb3RW.js';
4
4
 
5
5
  type MachinaVueSlotProps<TViewData = unknown, TNodeData = unknown> = {
6
6
  id: NodeId;
package/dist/vue/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  toResolvedTree
3
- } from "../chunk-TR24ERZT.js";
3
+ } from "../chunk-SVWYWI7I.js";
4
+ import "../chunk-VREK57S3.js";
4
5
 
5
6
  // src/vue/MachinaVueView.ts
6
7
  import { computed, defineComponent, h } from "vue";
@@ -0,0 +1,108 @@
1
+ # DeusMachina
2
+
3
+ DeusMachina is MachinaLayout's tiny behavioral kernel. It is deliberately small: utility judgment, explicit row-first state machines, stack-style state paths, deterministic stepping, and trace output.
4
+
5
+ ```ts
6
+ import { defineDeusMachine, judgeUtility, stepDeusMachine } from "machinalayout/deus";
7
+ ```
8
+
9
+ ## Contract summary
10
+
11
+ DeusMachina combines three narrow pieces:
12
+
13
+ 1. `judgeUtility(context, candidates, options)` for deterministic utility selection.
14
+ 2. `defineDeusMachine(machine)` for validating row-first stack HFSM definitions.
15
+ 3. `stepDeusMachine(machine, snapshot, event)` for one synchronous deterministic step with trace output.
16
+
17
+ State paths are non-empty arrays of non-empty strings. Segments are not trimmed; empty or whitespace-only segments are invalid. `formatDeusPath(["a", "b"])` formats paths as `a/b`. DeusMachina treats a path as its own ancestor for transition candidate collection.
18
+
19
+ ## Row-first authoring
20
+
21
+ Machines are authored as arrays, not nested objects:
22
+
23
+ ```ts
24
+ defineDeusMachine({
25
+ initial: ["debugOverlay", "collapsed"],
26
+ states: [{ path: ["debugOverlay", "collapsed"] }],
27
+ transitions: [{ key: "show", from: ["debugOverlay", "collapsed"], event: "showOverlay" }],
28
+ });
29
+ ```
30
+
31
+ Hierarchy comes from stack paths such as `debugOverlay/nonInteractiveOverlay`, not from nested authoring syntax.
32
+
33
+ ## Mutable board convention
34
+
35
+ Snapshots keep the board by reference. `stepDeusMachine` returns a new snapshot object and a copied state path, but it does not clone the board. User actions may mutate the board intentionally. Machine definitions are treated as immutable and are copied where DeusMachina normalizes paths.
36
+
37
+ ## Purity rules
38
+
39
+ `when`, `score`, and `reason` should be pure. They may read the board and event, but should not mutate them. `do`, `onEnter`, and `onExit` may mutate the board. DeusMachina does not sandbox user functions, so accidental mutation from guards, scores, or reasons is possible and is the caller's responsibility.
40
+
41
+ User errors are not swallowed. If a guard, score, reason, action, enter hook, or exit hook throws, the error propagates to the caller.
42
+
43
+ ## Transition selection semantics
44
+
45
+ A step gathers candidate transitions from the exact current state path, then parent paths upward to root. Candidate grouping is leaf first, then parent, then grandparent. Within each `from` path, author transition order is preserved.
46
+
47
+ An omitted transition `event` accepts any event. Otherwise, the transition event must equal `event.type`. An omitted `when` means eligible; a false `when` makes the transition ineligible and keeps it in the trace with score `0`.
48
+
49
+ Eligible transitions score as follows:
50
+
51
+ - omitted transition `score`: `1`
52
+ - numeric or function transition `score`: that finite score
53
+ - utility transition with no explicit transition `score`: selected utility candidate score
54
+
55
+ All gathered eligible candidates compete by score regardless of depth. Highest score wins. Ties are stable by candidate search order, so a leaf transition beats an equal-scored parent transition, and earlier author rows beat later rows within the same path.
56
+
57
+ If no transition is selected, state and board reference are unchanged, but `stepIndex` increments because a step occurred. If a selected transition has no `to`, state remains the same. Same-state transitions do not run exit or enter hooks.
58
+
59
+ Dynamic `to` functions are validated at runtime. They must return a valid path that exists in the machine.
60
+
61
+ ## Utility judgment semantics
62
+
63
+ `judgeUtility` preserves candidate array order in its trace. Omitted `when` means eligible. A false `when` makes the candidate ineligible with score `0`, and score functions are evaluated only for eligible candidates. Reason strings or functions are included in trace entries; reason functions are evaluated while building trace entries.
64
+
65
+ Scores must be finite. If no candidate is eligible, `selected` is `null`. Highest score wins and ties are stable by candidate order.
66
+
67
+ Hysteresis accepts a finite margin greater than or equal to `0`. If `previousKey` is present and the previous candidate is still eligible, a challenger must satisfy `challengerScore >= previousScore + margin` to replace it. If margin is `0`, normal highest-score semantics apply. If the previous candidate is missing or ineligible, normal selection applies.
68
+
69
+ `judgeUtility` does not mutate candidate definitions or the context itself, though user-provided functions can mutate external objects if they choose to.
70
+
71
+ ## Utility transition semantics
72
+
73
+ A transition with `utility` evaluates utility candidates only after its event and `when` checks pass. If no utility candidate is selected, the transition is not eligible and neither utility `do` nor transition `do` runs.
74
+
75
+ If a utility candidate is selected, its judgment appears in the transition trace. The transition score is the explicit transition score when provided; otherwise it is the selected utility candidate score. Transition `hysteresis` is passed through to the utility judgment via `hysteresis.previous(board)` and `hysteresis.margin`; `undefined` from `previous` means no previous key.
76
+
77
+ Utility candidate `do` receives the original board and event and runs before the outer transition `do`.
78
+
79
+ ## Enter, exit, and action order
80
+
81
+ When state changes, execution order is:
82
+
83
+ 1. `onExit` for states being exited, deepest first.
84
+ 2. Selected utility candidate `do`, if present.
85
+ 3. Transition `do`, if present.
86
+ 4. `onEnter` for states being entered, shallow to deep.
87
+
88
+ For `root/a/x -> root/b/y`, exit order is `root/a/x`, then `root/a`; enter order is `root/b`, then `root/b/y`. For `root/a/x -> root/a/y`, only `root/a/x` exits and only `root/a/y` enters. For `root/a -> root/a`, no exit or enter hooks run.
89
+
90
+ ## Trace contract
91
+
92
+ `stepDeusMachine` returns a trace containing state before, state after, event type, considered transitions, selected transition, transition eligibility, transition score, transition search index, reasons when provided, and inner utility judgment when applicable.
93
+
94
+ Trace data is intended to be JSON-serializable. It does not include function references, the board object, or arbitrary event payloads. `formatDeusStepTrace(trace)` provides a small deterministic one-line summary for selected and unselected steps.
95
+
96
+ ## Debug overlay helper
97
+
98
+ `createMachinaDebugOverlayMachine()` returns a validated DeusMachina machine for the controlled React debug overlay modes. `getMachinaDebugOverlayBehavior(board)` maps the board to rendering behavior:
99
+
100
+ - `collapsed`: not visible, `pointerEvents: "none"`, consumes no layout space.
101
+ - `nonInteractiveOverlay`: visible, `pointerEvents: "none"`, consumes no layout space.
102
+ - `interactivePanel`: visible, `pointerEvents: "auto"`, consumes layout space.
103
+
104
+ Labels and borders remain controlled by the board booleans; `false` stays false in overlay and panel modes.
105
+
106
+ ## Non-goals
107
+
108
+ DeusMachina intentionally does not include async workflows, tools, actors, persistence, LLM calls, schedulers, nested authoring syntax, uncontrolled React state, or a visual editor.
@@ -47,6 +47,8 @@ This document summarizes public layout and text diagnostic codes.
47
47
  - Note: this code name is historical and stable; do not rename it.
48
48
  - `StackContentNegative` — stack content space became negative. Typical cause: padding/gaps exceed container space.
49
49
  - `StackOverflow` — stack children exceed available axis space. Typical cause: fixed sizes + gaps exceed container.
50
+ - `ExpectedStackArrange` — a stack query helper was called on a non-stack node. Typical cause: using stack-only geometry helpers with a plain or grid parent.
51
+ - `StackQueryInvalidRange` — a remaining stack rectangle query produced a negative interval. Typical cause: `afterChildren` resolve after `beforeChildren`.
50
52
 
51
53
  ### Grid
52
54
 
@@ -66,6 +68,15 @@ This document summarizes public layout and text diagnostic codes.
66
68
  - `InvalidGuideFrame` — guide frame declaration is malformed. Typical cause: incomplete or conflicting guide spec.
67
69
  - `GuideTargetUnresolved` — guide target exists but was not resolved when needed. Typical cause: invalid dependency order/cycle.
68
70
 
71
+ ### Screen catalog and viewport matrix
72
+
73
+ - `InvalidViewport` — viewport metadata is malformed. Typical cause: blank key, non-positive dimensions, invalid `deviceScaleFactor`, or invalid lightweight metadata.
74
+ - `DuplicateViewportKey` — two viewport presets share one key. Typical cause: duplicate matrix entries.
75
+ - `UnknownViewportKey` — a requested or screen-referenced viewport key is absent from the matrix. Typical cause: typo or filtered matrix mismatch.
76
+ - `InvalidScreen` — screen catalog metadata is malformed. Typical cause: blank key, blank route, or invalid lightweight metadata.
77
+ - `DuplicateScreenKey` — two screen definitions share one key. Typical cause: duplicate catalog entries.
78
+ - `UnknownScreenKey` — a requested screen key is absent from the catalog. Typical cause: typo in an expansion filter.
79
+
69
80
  ### Variants
70
81
 
71
82
  - `InvalidVariantCondition` — variant condition is invalid. Typical cause: unsupported operator/value shape.