@protoyard/blueprint 0.0.0-stage → 1.1.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 (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1149 -2
  3. package/dist/aggregate.d.ts +83 -0
  4. package/dist/aggregate.js +268 -0
  5. package/dist/app-model.d.ts +76 -0
  6. package/dist/app-model.js +287 -0
  7. package/dist/archetypes/index.d.ts +1 -0
  8. package/dist/archetypes/index.js +1 -0
  9. package/dist/archetypes/schema.d.ts +126 -0
  10. package/dist/archetypes/schema.js +77 -0
  11. package/dist/audit.d.ts +52 -0
  12. package/dist/audit.js +149 -0
  13. package/dist/authoring/deck-lint.d.ts +32 -0
  14. package/dist/authoring/deck-lint.js +567 -0
  15. package/dist/authoring/index.d.ts +1 -0
  16. package/dist/authoring/index.js +1 -0
  17. package/dist/authoring/select-deck-layouts.d.ts +50 -0
  18. package/dist/authoring/select-deck-layouts.js +213 -0
  19. package/dist/authoring-apis.d.ts +13 -0
  20. package/dist/authoring-apis.js +13 -0
  21. package/dist/authoring-validation.d.ts +65 -0
  22. package/dist/authoring-validation.js +267 -0
  23. package/dist/avatar.d.ts +25 -0
  24. package/dist/avatar.js +30 -0
  25. package/dist/brand.d.ts +70 -0
  26. package/dist/brand.js +294 -0
  27. package/dist/brief/archetypes.d.ts +28 -0
  28. package/dist/brief/archetypes.js +798 -0
  29. package/dist/brief/content-synthesize.d.ts +31 -0
  30. package/dist/brief/content-synthesize.js +210 -0
  31. package/dist/brief/deck-outlines.d.ts +96 -0
  32. package/dist/brief/deck-outlines.js +561 -0
  33. package/dist/brief/draft-bodies.d.ts +29 -0
  34. package/dist/brief/draft-bodies.js +224 -0
  35. package/dist/brief/fields.d.ts +19 -0
  36. package/dist/brief/fields.js +229 -0
  37. package/dist/brief/index.d.ts +8 -0
  38. package/dist/brief/index.js +8 -0
  39. package/dist/brief/kind-detection.d.ts +69 -0
  40. package/dist/brief/kind-detection.js +353 -0
  41. package/dist/brief/limits.d.ts +117 -0
  42. package/dist/brief/limits.js +217 -0
  43. package/dist/brief/page-doc-outlines.d.ts +80 -0
  44. package/dist/brief/page-doc-outlines.js +485 -0
  45. package/dist/brief/synthesize.d.ts +59 -0
  46. package/dist/brief/synthesize.js +511 -0
  47. package/dist/brief/text.d.ts +13 -0
  48. package/dist/brief/text.js +59 -0
  49. package/dist/canvas-screen.d.ts +31 -0
  50. package/dist/canvas-screen.js +33 -0
  51. package/dist/canvas-seed.d.ts +58 -0
  52. package/dist/canvas-seed.js +82 -0
  53. package/dist/computed/evaluate.d.ts +11 -0
  54. package/dist/computed/evaluate.js +101 -0
  55. package/dist/computed/index.d.ts +4 -0
  56. package/dist/computed/index.js +3 -0
  57. package/dist/computed/public.d.ts +14 -0
  58. package/dist/computed/public.js +19 -0
  59. package/dist/computed/schema.d.ts +10 -0
  60. package/dist/computed/schema.js +140 -0
  61. package/dist/computed/types.d.ts +70 -0
  62. package/dist/computed/types.js +1 -0
  63. package/dist/computed/validate.d.ts +6 -0
  64. package/dist/computed/validate.js +146 -0
  65. package/dist/csv-guard.d.ts +15 -0
  66. package/dist/csv-guard.js +29 -0
  67. package/dist/csv.d.ts +85 -0
  68. package/dist/csv.js +288 -0
  69. package/dist/files.d.ts +35 -0
  70. package/dist/files.js +246 -0
  71. package/dist/form-schema.d.ts +124 -0
  72. package/dist/form-schema.js +300 -0
  73. package/dist/generator-version.d.ts +11 -0
  74. package/dist/generator-version.js +51 -0
  75. package/dist/generator.d.ts +34 -0
  76. package/dist/generator.js +445 -0
  77. package/dist/identity.d.ts +34 -0
  78. package/dist/identity.js +91 -0
  79. package/dist/import/csv-shape.d.ts +40 -0
  80. package/dist/import/csv-shape.js +265 -0
  81. package/dist/import/json-shape.d.ts +55 -0
  82. package/dist/import/json-shape.js +491 -0
  83. package/dist/import/limits.d.ts +13 -0
  84. package/dist/import/limits.js +14 -0
  85. package/dist/import/sqlite-schema.d.ts +36 -0
  86. package/dist/import/sqlite-schema.js +729 -0
  87. package/dist/index.d.ts +60 -0
  88. package/dist/index.js +58 -0
  89. package/dist/kinds/anchors.d.ts +18 -0
  90. package/dist/kinds/anchors.js +46 -0
  91. package/dist/kinds/bodies.d.ts +6 -0
  92. package/dist/kinds/bodies.js +32 -0
  93. package/dist/kinds/brand-runtime.d.ts +19 -0
  94. package/dist/kinds/brand-runtime.js +35 -0
  95. package/dist/kinds/deck.d.ts +8 -0
  96. package/dist/kinds/deck.js +34 -0
  97. package/dist/kinds/doc.d.ts +19 -0
  98. package/dist/kinds/doc.js +41 -0
  99. package/dist/kinds/embed/index.d.ts +39 -0
  100. package/dist/kinds/embed/index.js +91 -0
  101. package/dist/kinds/envelope.d.ts +159 -0
  102. package/dist/kinds/envelope.js +78 -0
  103. package/dist/kinds/index.d.ts +11 -0
  104. package/dist/kinds/index.js +11 -0
  105. package/dist/kinds/kind.d.ts +36 -0
  106. package/dist/kinds/kind.js +55 -0
  107. package/dist/kinds/limits.d.ts +64 -0
  108. package/dist/kinds/limits.js +190 -0
  109. package/dist/kinds/page-doc-runtime.d.ts +37 -0
  110. package/dist/kinds/page-doc-runtime.js +55 -0
  111. package/dist/kinds/page.d.ts +16 -0
  112. package/dist/kinds/page.js +67 -0
  113. package/dist/kinds/parse.d.ts +18 -0
  114. package/dist/kinds/parse.js +27 -0
  115. package/dist/kinds/runtime.d.ts +128 -0
  116. package/dist/kinds/runtime.js +206 -0
  117. package/dist/kinds/services.d.ts +49 -0
  118. package/dist/kinds/services.js +249 -0
  119. package/dist/kinds/testing.d.ts +9 -0
  120. package/dist/kinds/testing.js +16 -0
  121. package/dist/migrations.d.ts +97 -0
  122. package/dist/migrations.js +351 -0
  123. package/dist/motion.d.ts +50 -0
  124. package/dist/motion.js +52 -0
  125. package/dist/notifications.d.ts +19 -0
  126. package/dist/notifications.js +177 -0
  127. package/dist/patch.d.ts +122 -0
  128. package/dist/patch.js +322 -0
  129. package/dist/policy.d.ts +23 -0
  130. package/dist/policy.js +105 -0
  131. package/dist/runtime-definition.d.ts +39 -0
  132. package/dist/runtime-definition.js +74 -0
  133. package/dist/scenarios.d.ts +9 -0
  134. package/dist/scenarios.js +49 -0
  135. package/dist/schema/blueprint.schema.json +10191 -0
  136. package/dist/schema-json.d.ts +6 -0
  137. package/dist/schema-json.js +36 -0
  138. package/dist/schema.d.ts +6348 -0
  139. package/dist/schema.js +1269 -0
  140. package/dist/search.d.ts +16 -0
  141. package/dist/search.js +96 -0
  142. package/dist/services.d.ts +89 -0
  143. package/dist/services.js +898 -0
  144. package/dist/share.d.ts +78 -0
  145. package/dist/share.js +234 -0
  146. package/dist/sheet-screen.d.ts +49 -0
  147. package/dist/sheet-screen.js +91 -0
  148. package/dist/storage.d.ts +43 -0
  149. package/dist/storage.js +179 -0
  150. package/dist/stream.d.ts +26 -0
  151. package/dist/stream.js +173 -0
  152. package/dist/untrusted.d.ts +63 -0
  153. package/dist/untrusted.js +502 -0
  154. package/dist/validation-tasks.d.ts +178 -0
  155. package/dist/validation-tasks.js +199 -0
  156. package/dist/validation.d.ts +35 -0
  157. package/dist/validation.js +312 -0
  158. package/dist/variants.d.ts +112 -0
  159. package/dist/variants.js +283 -0
  160. package/dist/workflows/index.d.ts +3 -0
  161. package/dist/workflows/index.js +3 -0
  162. package/dist/workflows/schema.d.ts +50 -0
  163. package/dist/workflows/schema.js +34 -0
  164. package/dist/workflows/transition.d.ts +15 -0
  165. package/dist/workflows/transition.js +25 -0
  166. package/dist/workflows/validate.d.ts +17 -0
  167. package/dist/workflows/validate.js +140 -0
  168. package/package.json +120 -3
@@ -0,0 +1,58 @@
1
+ import type { Rng } from '@protoyard/core';
2
+ import type { Blueprint } from './schema.ts';
3
+ /**
4
+ * Seeded canvases for the `json` fields a `canvas` screen stores its boards
5
+ * in. Without this, the generator's generic `json` value (`{ note }`) is not
6
+ * a Texuli canvas, so every seeded record would open as a canvas that cannot
7
+ * be read. Each record gets a small planning board instead: a frame titled
8
+ * after the record with three sticky notes joined by connectors.
9
+ *
10
+ * Plain data in the Texuli canvas format (`texuli-canvas`, formatVersion 1):
11
+ * this module imports nothing from `@texuli/canvas`, and the canvas screen
12
+ * still validates every stored value as untrusted before it paints.
13
+ *
14
+ * Determinism: only the `rng` passed in, no Faker and no clock.
15
+ */
16
+ /** Sticky prompts a seeded board picks three of, in this order. */
17
+ export declare const CANVAS_SEED_PROMPTS: readonly ["Goals for this round", "Open questions", "Risks to watch", "Next steps", "Ideas to try", "Decisions so far", "Who to involve", "Dependencies"];
18
+ /** `json` field names that back a canvas screen, by entity name. */
19
+ export declare function canvasFieldsByEntity(blueprint: Pick<Blueprint, 'screens'>): Map<string, Set<string>>;
20
+ /** A small planning board for one record: a titled frame, three stickies, two connectors. */
21
+ export declare function seedCanvas(rng: Rng, title: string): {
22
+ format: "texuli-canvas";
23
+ formatVersion: 1;
24
+ meta: {
25
+ title: string;
26
+ };
27
+ shapes: {
28
+ id: string;
29
+ type: "frame";
30
+ x: number;
31
+ y: number;
32
+ width: number;
33
+ height: number;
34
+ title: string;
35
+ children: {
36
+ id: string;
37
+ type: "sticky";
38
+ x: number;
39
+ y: number;
40
+ width: number;
41
+ height: number;
42
+ text: string;
43
+ color: "green" | "blue" | "yellow" | "pink" | "orange" | "purple";
44
+ }[];
45
+ }[];
46
+ connectors: {
47
+ id: string;
48
+ from: {
49
+ shape: string;
50
+ anchor: "auto";
51
+ };
52
+ to: {
53
+ shape: string;
54
+ anchor: "auto";
55
+ };
56
+ routing: "curved";
57
+ }[];
58
+ };
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Seeded canvases for the `json` fields a `canvas` screen stores its boards
3
+ * in. Without this, the generator's generic `json` value (`{ note }`) is not
4
+ * a Texuli canvas, so every seeded record would open as a canvas that cannot
5
+ * be read. Each record gets a small planning board instead: a frame titled
6
+ * after the record with three sticky notes joined by connectors.
7
+ *
8
+ * Plain data in the Texuli canvas format (`texuli-canvas`, formatVersion 1):
9
+ * this module imports nothing from `@texuli/canvas`, and the canvas screen
10
+ * still validates every stored value as untrusted before it paints.
11
+ *
12
+ * Determinism: only the `rng` passed in, no Faker and no clock.
13
+ */
14
+ /** Sticky prompts a seeded board picks three of, in this order. */
15
+ export const CANVAS_SEED_PROMPTS = [
16
+ 'Goals for this round',
17
+ 'Open questions',
18
+ 'Risks to watch',
19
+ 'Next steps',
20
+ 'Ideas to try',
21
+ 'Decisions so far',
22
+ 'Who to involve',
23
+ 'Dependencies',
24
+ ];
25
+ const STICKY_COLORS = ['yellow', 'green', 'blue', 'pink', 'orange', 'purple'];
26
+ /** Canvas titles are capped at 200 characters, like the screen's blank canvas. */
27
+ const MAX_TITLE = 200;
28
+ /** `json` field names that back a canvas screen, by entity name. */
29
+ export function canvasFieldsByEntity(blueprint) {
30
+ const byEntity = new Map();
31
+ for (const screen of blueprint.screens) {
32
+ if (screen.type !== 'canvas')
33
+ continue;
34
+ let fields = byEntity.get(screen.entity);
35
+ if (!fields) {
36
+ fields = new Set();
37
+ byEntity.set(screen.entity, fields);
38
+ }
39
+ fields.add(screen.field);
40
+ }
41
+ return byEntity;
42
+ }
43
+ /** A small planning board for one record: a titled frame, three stickies, two connectors. */
44
+ export function seedCanvas(rng, title) {
45
+ const name = title.trim().slice(0, MAX_TITLE) || 'Planning board';
46
+ const order = rng.sample(CANVAS_SEED_PROMPTS.map((_, index) => index), 3);
47
+ const prompts = order.sort((a, b) => a - b).map((index) => CANVAS_SEED_PROMPTS[index]);
48
+ const colors = rng.sample(STICKY_COLORS, 3);
49
+ const stickies = prompts.map((text, index) => ({
50
+ id: `seed-sticky-${index + 1}`,
51
+ type: 'sticky',
52
+ x: 40 + index * 220,
53
+ y: 96 + (index % 2) * 24,
54
+ width: 180,
55
+ height: 140,
56
+ text,
57
+ color: colors[index] ?? 'yellow',
58
+ }));
59
+ return {
60
+ format: 'texuli-canvas',
61
+ formatVersion: 1,
62
+ meta: { title: name },
63
+ shapes: [
64
+ {
65
+ id: 'seed-frame',
66
+ type: 'frame',
67
+ x: 0,
68
+ y: 0,
69
+ width: 720,
70
+ height: 320,
71
+ title: name,
72
+ children: stickies,
73
+ },
74
+ ],
75
+ connectors: stickies.slice(1).map((sticky, index) => ({
76
+ id: `seed-link-${index + 1}`,
77
+ from: { shape: stickies[index]?.id, anchor: 'auto' },
78
+ to: { shape: sticky.id, anchor: 'auto' },
79
+ routing: 'curved',
80
+ })),
81
+ };
82
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComputedEvaluationContext, ComputedValue } from './types.ts';
2
+ import { validateComputedExpression } from './validate.ts';
3
+ export type ComputedEvaluationResult = {
4
+ success: true;
5
+ value: ComputedValue;
6
+ } | {
7
+ success: false;
8
+ diagnostics: ReturnType<typeof validateComputedExpression>['diagnostics'];
9
+ };
10
+ /** Validate and evaluate a read-only AST. Missing or invalid row values propagate as null. */
11
+ export declare function evaluateComputedExpression(expression: unknown, context: ComputedEvaluationContext): ComputedEvaluationResult;
@@ -0,0 +1,101 @@
1
+ import { validateComputedExpression } from "./validate.js";
2
+ function ownValue(record, key) {
3
+ return record && Object.hasOwn(record, key) ? record[key] : null;
4
+ }
5
+ function asMilliseconds(value) {
6
+ if (typeof value === 'number')
7
+ return Number.isFinite(value) ? value : null;
8
+ if (typeof value !== 'string')
9
+ return null;
10
+ const parsed = Date.parse(value);
11
+ return Number.isFinite(parsed) ? parsed : null;
12
+ }
13
+ function relatedRows(value) {
14
+ if (Array.isArray(value))
15
+ return value.filter(isRecord);
16
+ return isRecord(value) ? [value] : [];
17
+ }
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ }
21
+ function finiteOrNull(value) {
22
+ return Number.isFinite(value) ? value : null;
23
+ }
24
+ function evaluate(expression, context) {
25
+ if (expression.op === 'literal')
26
+ return expression.value;
27
+ if (expression.op === 'field') {
28
+ const value = ownValue(context.row, expression.field);
29
+ if (typeof value === 'string' || typeof value === 'boolean' || value === null)
30
+ return value;
31
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
32
+ }
33
+ if (expression.op === 'concat') {
34
+ const values = expression.values.map((child) => evaluate(child, context));
35
+ return values.every((value) => typeof value === 'string') ? values.join('') : null;
36
+ }
37
+ if (expression.op === 'dateDiff') {
38
+ const start = asMilliseconds(evaluate(expression.start, context));
39
+ const end = asMilliseconds(evaluate(expression.end, context));
40
+ if (start === null || end === null)
41
+ return null;
42
+ const divisor = {
43
+ milliseconds: 1,
44
+ seconds: 1_000,
45
+ minutes: 60_000,
46
+ hours: 3_600_000,
47
+ days: 86_400_000,
48
+ }[expression.unit];
49
+ return finiteOrNull((end - start) / divisor);
50
+ }
51
+ if (expression.op === 'aggregate') {
52
+ const rows = relatedRows(ownValue(context.relations, expression.relation));
53
+ if (expression.aggregate === 'count')
54
+ return rows.length;
55
+ const values = rows
56
+ .map((row) => ownValue(row, expression.field ?? ''))
57
+ .filter((value) => value !== null && value !== undefined);
58
+ if (values.length === 0)
59
+ return null;
60
+ if (expression.aggregate === 'min' || expression.aggregate === 'max') {
61
+ const first = values[0];
62
+ if (typeof first === 'string') {
63
+ const dates = values.map(asMilliseconds);
64
+ if (dates.every((date) => date !== null)) {
65
+ const target = dates.reduce((best, date) => expression.aggregate === 'min' ? Math.min(best, date) : Math.max(best, date));
66
+ return values.find((value) => asMilliseconds(value) === target);
67
+ }
68
+ }
69
+ const numbers = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
70
+ if (numbers.length !== values.length)
71
+ return null;
72
+ return numbers.reduce((best, number) => expression.aggregate === 'min' ? Math.min(best, number) : Math.max(best, number));
73
+ }
74
+ const numbers = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
75
+ if (numbers.length !== values.length)
76
+ return null;
77
+ const sum = numbers.reduce((total, value) => total + value, 0);
78
+ return expression.aggregate === 'sum' ? finiteOrNull(sum) : finiteOrNull(sum / numbers.length);
79
+ }
80
+ const left = evaluate(expression.left, context);
81
+ const right = evaluate(expression.right, context);
82
+ if (typeof left !== 'number' || typeof right !== 'number')
83
+ return null;
84
+ let value;
85
+ if (expression.op === 'add')
86
+ value = left + right;
87
+ else if (expression.op === 'subtract')
88
+ value = left - right;
89
+ else if (expression.op === 'multiply')
90
+ value = left * right;
91
+ else
92
+ value = right === 0 ? Number.NaN : left / right;
93
+ return Number.isFinite(value) ? value : null;
94
+ }
95
+ /** Validate and evaluate a read-only AST. Missing or invalid row values propagate as null. */
96
+ export function evaluateComputedExpression(expression, context) {
97
+ const validation = validateComputedExpression(expression, context);
98
+ if (!validation.valid)
99
+ return { success: false, diagnostics: validation.diagnostics };
100
+ return { success: true, value: evaluate(expression, context) };
101
+ }
@@ -0,0 +1,4 @@
1
+ export { type ComputedEvaluationResult, evaluateComputedExpression } from './evaluate.ts';
2
+ export { ComputedExpressionError, MAX_COMPUTED_EXPRESSION_DEPTH, MAX_COMPUTED_EXPRESSION_NODES, parseComputedExpression, safeParseComputedExpression, } from './public.ts';
3
+ export type { ComputedDiagnostic, ComputedEvaluationContext, ComputedExpression, ComputedExpressionContext, ComputedValidationResult, ComputedValue, ComputedValueType, } from './types.ts';
4
+ export { computedContext, validateComputedExpression } from './validate.ts';
@@ -0,0 +1,3 @@
1
+ export { evaluateComputedExpression } from "./evaluate.js";
2
+ export { ComputedExpressionError, MAX_COMPUTED_EXPRESSION_DEPTH, MAX_COMPUTED_EXPRESSION_NODES, parseComputedExpression, safeParseComputedExpression, } from "./public.js";
3
+ export { computedContext, validateComputedExpression } from "./validate.js";
@@ -0,0 +1,14 @@
1
+ import type { ComputedDiagnostic, ComputedExpression } from './types.ts';
2
+ export { MAX_COMPUTED_EXPRESSION_DEPTH, MAX_COMPUTED_EXPRESSION_NODES, } from './schema.ts';
3
+ export declare class ComputedExpressionError extends Error {
4
+ readonly diagnostics: ComputedDiagnostic[];
5
+ constructor(diagnostics: ComputedDiagnostic[]);
6
+ }
7
+ export declare function safeParseComputedExpression(input: unknown): {
8
+ success: true;
9
+ data: ComputedExpression;
10
+ } | {
11
+ success: false;
12
+ diagnostics: ComputedDiagnostic[];
13
+ };
14
+ export declare function parseComputedExpression(input: unknown): ComputedExpression;
@@ -0,0 +1,19 @@
1
+ import { parseExpressionShape } from "./schema.js";
2
+ export { MAX_COMPUTED_EXPRESSION_DEPTH, MAX_COMPUTED_EXPRESSION_NODES, } from "./schema.js";
3
+ export class ComputedExpressionError extends Error {
4
+ diagnostics;
5
+ constructor(diagnostics) {
6
+ super(diagnostics.map((diagnostic) => `${diagnostic.jsonPath}: ${diagnostic.message}`).join('\n'));
7
+ this.name = 'ComputedExpressionError';
8
+ this.diagnostics = diagnostics;
9
+ }
10
+ }
11
+ export function safeParseComputedExpression(input) {
12
+ return parseExpressionShape(input);
13
+ }
14
+ export function parseComputedExpression(input) {
15
+ const result = parseExpressionShape(input);
16
+ if (!result.success)
17
+ throw new ComputedExpressionError(result.diagnostics);
18
+ return result.data;
19
+ }
@@ -0,0 +1,10 @@
1
+ import type { ComputedDiagnostic, ComputedExpression } from './types.ts';
2
+ export declare const MAX_COMPUTED_EXPRESSION_DEPTH = 32;
3
+ export declare const MAX_COMPUTED_EXPRESSION_NODES = 256;
4
+ export declare function parseExpressionShape(input: unknown): {
5
+ success: true;
6
+ data: ComputedExpression;
7
+ } | {
8
+ success: false;
9
+ diagnostics: ComputedDiagnostic[];
10
+ };
@@ -0,0 +1,140 @@
1
+ import { z } from 'zod';
2
+ export const MAX_COMPUTED_EXPRESSION_DEPTH = 32;
3
+ export const MAX_COMPUTED_EXPRESSION_NODES = 256;
4
+ const fieldName = z.string().regex(/^[a-z][a-zA-Z0-9]*$/, {
5
+ message: 'field references must use lowerCamelCase names',
6
+ });
7
+ const scalar = z.union([z.string().max(1_000), z.number().finite(), z.boolean(), z.null()]);
8
+ const ExpressionSchema = z.lazy(() => z.discriminatedUnion('op', [
9
+ z.strictObject({ op: z.literal('literal'), value: scalar }),
10
+ z.strictObject({ op: z.literal('field'), field: fieldName }),
11
+ ...['add', 'subtract', 'multiply', 'divide'].map((op) => z.strictObject({ op: z.literal(op), left: ExpressionSchema, right: ExpressionSchema })),
12
+ z.strictObject({ op: z.literal('concat'), values: z.array(ExpressionSchema).min(2).max(32) }),
13
+ z.strictObject({
14
+ op: z.literal('dateDiff'),
15
+ start: ExpressionSchema,
16
+ end: ExpressionSchema,
17
+ unit: z.enum(['milliseconds', 'seconds', 'minutes', 'hours', 'days']),
18
+ }),
19
+ z.strictObject({
20
+ op: z.literal('aggregate'),
21
+ relation: fieldName,
22
+ aggregate: z.enum(['count', 'sum', 'avg', 'min', 'max']),
23
+ field: fieldName.optional(),
24
+ }),
25
+ ]));
26
+ /** Reject non-JSON containers, cycles and excessive AST size before recursive Zod parsing. */
27
+ function preflight(input) {
28
+ const seen = new WeakSet();
29
+ const stack = [
30
+ { value: input, depth: 1, path: [] },
31
+ ];
32
+ let nodes = 0;
33
+ while (stack.length > 0) {
34
+ const current = stack.pop();
35
+ if (!current)
36
+ continue;
37
+ const value = current.value;
38
+ if (value === null || typeof value !== 'object')
39
+ continue;
40
+ if (seen.has(value)) {
41
+ return {
42
+ code: 'cyclic-expression',
43
+ path: current.path,
44
+ message: 'expression data must not contain cycles',
45
+ };
46
+ }
47
+ seen.add(value);
48
+ nodes += 1;
49
+ if (nodes > MAX_COMPUTED_EXPRESSION_NODES) {
50
+ return {
51
+ code: 'expression-too-large',
52
+ path: current.path,
53
+ message: `expression exceeds ${MAX_COMPUTED_EXPRESSION_NODES} object or array nodes`,
54
+ };
55
+ }
56
+ if (current.depth > MAX_COMPUTED_EXPRESSION_DEPTH) {
57
+ return {
58
+ code: 'expression-too-deep',
59
+ path: current.path,
60
+ message: `expression exceeds depth ${MAX_COMPUTED_EXPRESSION_DEPTH}`,
61
+ };
62
+ }
63
+ if (Array.isArray(value)) {
64
+ if (value.length > MAX_COMPUTED_EXPRESSION_NODES) {
65
+ return {
66
+ code: 'expression-too-large',
67
+ path: current.path,
68
+ message: `expression array exceeds ${MAX_COMPUTED_EXPRESSION_NODES} entries`,
69
+ };
70
+ }
71
+ for (let index = value.length - 1; index >= 0; index -= 1) {
72
+ stack.push({
73
+ value: value[index],
74
+ depth: current.depth + 1,
75
+ path: [...current.path, index],
76
+ });
77
+ }
78
+ continue;
79
+ }
80
+ const prototype = Object.getPrototypeOf(value);
81
+ if (prototype !== Object.prototype && prototype !== null) {
82
+ return {
83
+ code: 'invalid-container',
84
+ path: current.path,
85
+ message: 'expression objects must be plain JSON objects',
86
+ };
87
+ }
88
+ const keys = Object.keys(value);
89
+ if (keys.length > MAX_COMPUTED_EXPRESSION_NODES) {
90
+ return {
91
+ code: 'expression-too-large',
92
+ path: current.path,
93
+ message: `expression object exceeds ${MAX_COMPUTED_EXPRESSION_NODES} properties`,
94
+ };
95
+ }
96
+ for (const key of keys.reverse()) {
97
+ stack.push({
98
+ value: value[key],
99
+ depth: current.depth + 1,
100
+ path: [...current.path, key],
101
+ });
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ function jsonPath(path) {
107
+ let result = '$';
108
+ for (const segment of path) {
109
+ result += typeof segment === 'number' ? `[${segment}]` : `[${JSON.stringify(segment)}]`;
110
+ }
111
+ return result;
112
+ }
113
+ export function parseExpressionShape(input) {
114
+ const sizeFailure = preflight(input);
115
+ if (sizeFailure) {
116
+ return {
117
+ success: false,
118
+ diagnostics: [
119
+ {
120
+ ...sizeFailure,
121
+ jsonPath: jsonPath(sizeFailure.path),
122
+ hint: 'Simplify the expression and use only bounded JSON expression nodes.',
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ const result = ExpressionSchema.safeParse(input);
128
+ if (result.success)
129
+ return { success: true, data: result.data };
130
+ return {
131
+ success: false,
132
+ diagnostics: result.error.issues.map((issue) => ({
133
+ code: `schema-${issue.code}`,
134
+ path: issue.path.filter((segment) => typeof segment !== 'symbol'),
135
+ jsonPath: jsonPath(issue.path.filter((segment) => typeof segment !== 'symbol')),
136
+ message: issue.message,
137
+ hint: 'Use the documented expression operator and its required fields; executable strings are not expressions.',
138
+ })),
139
+ };
140
+ }
@@ -0,0 +1,70 @@
1
+ /** The closed JSON AST used for deterministic, read-time computed values. */
2
+ export type ComputedExpression = {
3
+ op: 'literal';
4
+ value: string | number | boolean | null;
5
+ } | {
6
+ op: 'field';
7
+ field: string;
8
+ } | {
9
+ op: 'add' | 'subtract' | 'multiply' | 'divide';
10
+ left: ComputedExpression;
11
+ right: ComputedExpression;
12
+ } | {
13
+ op: 'concat';
14
+ values: ComputedExpression[];
15
+ } | {
16
+ op: 'dateDiff';
17
+ start: ComputedExpression;
18
+ end: ComputedExpression;
19
+ unit: 'milliseconds' | 'seconds' | 'minutes' | 'hours' | 'days';
20
+ } | {
21
+ op: 'aggregate';
22
+ relation: string;
23
+ aggregate: 'count' | 'sum' | 'avg' | 'min' | 'max';
24
+ field?: string;
25
+ };
26
+ /** Static types supported by computed expressions. */
27
+ export type ComputedValueType = 'number' | 'string' | 'boolean' | 'date' | 'null';
28
+ /** JSON-compatible computed result; missing input values propagate as null. */
29
+ export type ComputedValue = string | number | boolean | null;
30
+ export type ComputedDiagnostic = {
31
+ code: string;
32
+ path: (string | number)[];
33
+ jsonPath: string;
34
+ message: string;
35
+ hint: string;
36
+ };
37
+ export type ComputedExpressionContext = {
38
+ /** Entity whose row is passed to field expressions. */
39
+ sourceEntity: {
40
+ name: string;
41
+ fields: readonly {
42
+ name: string;
43
+ kind: string;
44
+ required?: boolean;
45
+ to?: string;
46
+ }[];
47
+ };
48
+ /** All blueprint entities, used to resolve relation targets. */
49
+ entities: readonly {
50
+ name: string;
51
+ fields: readonly {
52
+ name: string;
53
+ kind: string;
54
+ required?: boolean;
55
+ to?: string;
56
+ }[];
57
+ }[];
58
+ };
59
+ export type ComputedValidationResult = {
60
+ valid: boolean;
61
+ diagnostics: ComputedDiagnostic[];
62
+ resultType: ComputedValueType | null;
63
+ nullable: boolean;
64
+ };
65
+ export type ComputedEvaluationContext = ComputedExpressionContext & {
66
+ /** Current entity row. Values are read only and never mutated. */
67
+ row: Readonly<Record<string, unknown>>;
68
+ /** Related records grouped by relation field name; many relations use arrays. */
69
+ relations?: Readonly<Record<string, unknown>>;
70
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { BlueprintEntity } from '../schema.ts';
2
+ import type { ComputedExpressionContext, ComputedValidationResult } from './types.ts';
3
+ /** Validate AST shape, field references, operator input types and inferred output type. */
4
+ export declare function validateComputedExpression(input: unknown, context: ComputedExpressionContext): ComputedValidationResult;
5
+ /** Helper for callers that need to use the public entity shape as context. */
6
+ export declare function computedContext(sourceEntity: BlueprintEntity, entities: readonly BlueprintEntity[]): ComputedExpressionContext;