@ripple-ui/core 0.0.0-stage → 0.6.1-next.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/build/build-spec.d.ts +148 -0
  5. package/dist/build/build-spec.d.ts.map +1 -0
  6. package/dist/build/build-spec.js +297 -0
  7. package/dist/build/build-spec.js.map +1 -0
  8. package/dist/build/index.d.ts +6 -0
  9. package/dist/build/index.d.ts.map +1 -0
  10. package/dist/build/index.js +7 -0
  11. package/dist/build/index.js.map +1 -0
  12. package/dist/core/audit-report.d.ts +24 -0
  13. package/dist/core/audit-report.d.ts.map +1 -0
  14. package/dist/core/audit-report.js +12 -0
  15. package/dist/core/audit-report.js.map +1 -0
  16. package/dist/core/base-dispatcher.d.ts +115 -0
  17. package/dist/core/base-dispatcher.d.ts.map +1 -0
  18. package/dist/core/base-dispatcher.js +311 -0
  19. package/dist/core/base-dispatcher.js.map +1 -0
  20. package/dist/core/brand-applier.d.ts +39 -0
  21. package/dist/core/brand-applier.d.ts.map +1 -0
  22. package/dist/core/brand-applier.js +179 -0
  23. package/dist/core/brand-applier.js.map +1 -0
  24. package/dist/core/data-guard.d.ts +36 -0
  25. package/dist/core/data-guard.d.ts.map +1 -0
  26. package/dist/core/data-guard.js +232 -0
  27. package/dist/core/data-guard.js.map +1 -0
  28. package/dist/core/event-dispatcher.d.ts +218 -0
  29. package/dist/core/event-dispatcher.d.ts.map +1 -0
  30. package/dist/core/event-dispatcher.js +502 -0
  31. package/dist/core/event-dispatcher.js.map +1 -0
  32. package/dist/core/expression-resolver.d.ts +122 -0
  33. package/dist/core/expression-resolver.d.ts.map +1 -0
  34. package/dist/core/expression-resolver.js +1137 -0
  35. package/dist/core/expression-resolver.js.map +1 -0
  36. package/dist/core/flow-spec.d.ts +46 -0
  37. package/dist/core/flow-spec.d.ts.map +1 -0
  38. package/dist/core/flow-spec.js +69 -0
  39. package/dist/core/flow-spec.js.map +1 -0
  40. package/dist/core/index.d.ts +4 -0
  41. package/dist/core/index.d.ts.map +1 -0
  42. package/dist/core/index.js +4 -0
  43. package/dist/core/index.js.map +1 -0
  44. package/dist/core/normalizer.d.ts +16 -0
  45. package/dist/core/normalizer.d.ts.map +1 -0
  46. package/dist/core/normalizer.js +53 -0
  47. package/dist/core/normalizer.js.map +1 -0
  48. package/dist/core/spec-audit.d.ts +25 -0
  49. package/dist/core/spec-audit.d.ts.map +1 -0
  50. package/dist/core/spec-audit.js +389 -0
  51. package/dist/core/spec-audit.js.map +1 -0
  52. package/dist/core/spec-id.d.ts +27 -0
  53. package/dist/core/spec-id.d.ts.map +1 -0
  54. package/dist/core/spec-id.js +74 -0
  55. package/dist/core/spec-id.js.map +1 -0
  56. package/dist/core/spec-issues.d.ts +28 -0
  57. package/dist/core/spec-issues.d.ts.map +1 -0
  58. package/dist/core/spec-issues.js +62 -0
  59. package/dist/core/spec-issues.js.map +1 -0
  60. package/dist/core/spec-mutator.d.ts +121 -0
  61. package/dist/core/spec-mutator.d.ts.map +1 -0
  62. package/dist/core/spec-mutator.js +394 -0
  63. package/dist/core/spec-mutator.js.map +1 -0
  64. package/dist/core/state-mutator.d.ts +37 -0
  65. package/dist/core/state-mutator.d.ts.map +1 -0
  66. package/dist/core/state-mutator.js +215 -0
  67. package/dist/core/state-mutator.js.map +1 -0
  68. package/dist/core/state-store.d.ts +45 -0
  69. package/dist/core/state-store.d.ts.map +1 -0
  70. package/dist/core/state-store.js +25 -0
  71. package/dist/core/state-store.js.map +1 -0
  72. package/dist/core/text-coerce.d.ts +17 -0
  73. package/dist/core/text-coerce.d.ts.map +1 -0
  74. package/dist/core/text-coerce.js +61 -0
  75. package/dist/core/text-coerce.js.map +1 -0
  76. package/dist/core/theme-applier.d.ts +6 -0
  77. package/dist/core/theme-applier.d.ts.map +1 -0
  78. package/dist/core/theme-applier.js +63 -0
  79. package/dist/core/theme-applier.js.map +1 -0
  80. package/dist/core/validate-catalog.d.ts +35 -0
  81. package/dist/core/validate-catalog.d.ts.map +1 -0
  82. package/dist/core/validate-catalog.js +67 -0
  83. package/dist/core/validate-catalog.js.map +1 -0
  84. package/dist/core/widget-bind-contract.d.ts +36 -0
  85. package/dist/core/widget-bind-contract.d.ts.map +1 -0
  86. package/dist/core/widget-bind-contract.js +204 -0
  87. package/dist/core/widget-bind-contract.js.map +1 -0
  88. package/dist/core/widget-registry.d.ts +54 -0
  89. package/dist/core/widget-registry.d.ts.map +1 -0
  90. package/dist/core/widget-registry.js +88 -0
  91. package/dist/core/widget-registry.js.map +1 -0
  92. package/dist/headless/index.d.ts +34 -0
  93. package/dist/headless/index.d.ts.map +1 -0
  94. package/dist/headless/index.js +30 -0
  95. package/dist/headless/index.js.map +1 -0
  96. package/dist/headless/resolve-tree.d.ts +46 -0
  97. package/dist/headless/resolve-tree.d.ts.map +1 -0
  98. package/dist/headless/resolve-tree.js +232 -0
  99. package/dist/headless/resolve-tree.js.map +1 -0
  100. package/dist/headless/runtime-base.d.ts +94 -0
  101. package/dist/headless/runtime-base.d.ts.map +1 -0
  102. package/dist/headless/runtime-base.js +141 -0
  103. package/dist/headless/runtime-base.js.map +1 -0
  104. package/dist/headless/runtime.d.ts +49 -0
  105. package/dist/headless/runtime.d.ts.map +1 -0
  106. package/dist/headless/runtime.js +52 -0
  107. package/dist/headless/runtime.js.map +1 -0
  108. package/dist/headless/slim.d.ts +41 -0
  109. package/dist/headless/slim.d.ts.map +1 -0
  110. package/dist/headless/slim.js +41 -0
  111. package/dist/headless/slim.js.map +1 -0
  112. package/dist/headless/state.d.ts +36 -0
  113. package/dist/headless/state.d.ts.map +1 -0
  114. package/dist/headless/state.js +118 -0
  115. package/dist/headless/state.js.map +1 -0
  116. package/dist/headless/types.d.ts +95 -0
  117. package/dist/headless/types.d.ts.map +1 -0
  118. package/dist/headless/types.js +22 -0
  119. package/dist/headless/types.js.map +1 -0
  120. package/dist/index.d.ts +61 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +74 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/iso/audit.d.ts +111 -0
  125. package/dist/iso/audit.d.ts.map +1 -0
  126. package/dist/iso/audit.js +319 -0
  127. package/dist/iso/audit.js.map +1 -0
  128. package/dist/iso/enrich.d.ts +26 -0
  129. package/dist/iso/enrich.d.ts.map +1 -0
  130. package/dist/iso/enrich.js +256 -0
  131. package/dist/iso/enrich.js.map +1 -0
  132. package/dist/iso/index.d.ts +12 -0
  133. package/dist/iso/index.d.ts.map +1 -0
  134. package/dist/iso/index.js +12 -0
  135. package/dist/iso/index.js.map +1 -0
  136. package/dist/iso/kit.d.ts +194 -0
  137. package/dist/iso/kit.d.ts.map +1 -0
  138. package/dist/iso/kit.js +685 -0
  139. package/dist/iso/kit.js.map +1 -0
  140. package/dist/iso/scene.d.ts +168 -0
  141. package/dist/iso/scene.d.ts.map +1 -0
  142. package/dist/iso/scene.js +821 -0
  143. package/dist/iso/scene.js.map +1 -0
  144. package/dist/iso/solids.d.ts +49 -0
  145. package/dist/iso/solids.d.ts.map +1 -0
  146. package/dist/iso/solids.js +218 -0
  147. package/dist/iso/solids.js.map +1 -0
  148. package/dist/manifest/actions.d.ts +42 -0
  149. package/dist/manifest/actions.d.ts.map +1 -0
  150. package/dist/manifest/actions.js +303 -0
  151. package/dist/manifest/actions.js.map +1 -0
  152. package/dist/manifest/craft.d.ts +51 -0
  153. package/dist/manifest/craft.d.ts.map +1 -0
  154. package/dist/manifest/craft.js +458 -0
  155. package/dist/manifest/craft.js.map +1 -0
  156. package/dist/manifest/envelope.d.ts +35 -0
  157. package/dist/manifest/envelope.d.ts.map +1 -0
  158. package/dist/manifest/envelope.js +41 -0
  159. package/dist/manifest/envelope.js.map +1 -0
  160. package/dist/manifest/illustration-svg.d.ts +71 -0
  161. package/dist/manifest/illustration-svg.d.ts.map +1 -0
  162. package/dist/manifest/illustration-svg.js +79 -0
  163. package/dist/manifest/illustration-svg.js.map +1 -0
  164. package/dist/manifest/index.d.ts +24 -0
  165. package/dist/manifest/index.d.ts.map +1 -0
  166. package/dist/manifest/index.js +24 -0
  167. package/dist/manifest/index.js.map +1 -0
  168. package/dist/manifest/slim-widgets.d.ts +23 -0
  169. package/dist/manifest/slim-widgets.d.ts.map +1 -0
  170. package/dist/manifest/slim-widgets.js +95 -0
  171. package/dist/manifest/slim-widgets.js.map +1 -0
  172. package/dist/manifest/slim.d.ts +67 -0
  173. package/dist/manifest/slim.d.ts.map +1 -0
  174. package/dist/manifest/slim.js +48 -0
  175. package/dist/manifest/slim.js.map +1 -0
  176. package/dist/motion/engine.d.ts +19 -0
  177. package/dist/motion/engine.d.ts.map +1 -0
  178. package/dist/motion/engine.js +77 -0
  179. package/dist/motion/engine.js.map +1 -0
  180. package/dist/motion/load-tier1.d.ts +10 -0
  181. package/dist/motion/load-tier1.d.ts.map +1 -0
  182. package/dist/motion/load-tier1.js +24 -0
  183. package/dist/motion/load-tier1.js.map +1 -0
  184. package/dist/motion/presets.d.ts +84 -0
  185. package/dist/motion/presets.d.ts.map +1 -0
  186. package/dist/motion/presets.js +118 -0
  187. package/dist/motion/presets.js.map +1 -0
  188. package/dist/motion/reduce-motion.d.ts +3 -0
  189. package/dist/motion/reduce-motion.d.ts.map +1 -0
  190. package/dist/motion/reduce-motion.js +41 -0
  191. package/dist/motion/reduce-motion.js.map +1 -0
  192. package/dist/schema/brand.d.ts +185 -0
  193. package/dist/schema/brand.d.ts.map +1 -0
  194. package/dist/schema/brand.js +143 -0
  195. package/dist/schema/brand.js.map +1 -0
  196. package/dist/schema/event-handler.d.ts +959 -0
  197. package/dist/schema/event-handler.d.ts.map +1 -0
  198. package/dist/schema/event-handler.js +309 -0
  199. package/dist/schema/event-handler.js.map +1 -0
  200. package/dist/schema/index.d.ts +7 -0
  201. package/dist/schema/index.d.ts.map +1 -0
  202. package/dist/schema/index.js +12 -0
  203. package/dist/schema/index.js.map +1 -0
  204. package/dist/schema/motion.d.ts +154 -0
  205. package/dist/schema/motion.d.ts.map +1 -0
  206. package/dist/schema/motion.js +25 -0
  207. package/dist/schema/motion.js.map +1 -0
  208. package/dist/schema/ui-spec.d.ts +3610 -0
  209. package/dist/schema/ui-spec.d.ts.map +1 -0
  210. package/dist/schema/ui-spec.js +225 -0
  211. package/dist/schema/ui-spec.js.map +1 -0
  212. package/dist/schema/universal-spec.d.ts +195 -0
  213. package/dist/schema/universal-spec.d.ts.map +1 -0
  214. package/dist/schema/universal-spec.js +259 -0
  215. package/dist/schema/universal-spec.js.map +1 -0
  216. package/dist/schema/widget-types.d.ts +54 -0
  217. package/dist/schema/widget-types.d.ts.map +1 -0
  218. package/dist/schema/widget-types.js +37 -0
  219. package/dist/schema/widget-types.js.map +1 -0
  220. package/dist/security/safe-url.d.ts +53 -0
  221. package/dist/security/safe-url.d.ts.map +1 -0
  222. package/dist/security/safe-url.js +197 -0
  223. package/dist/security/safe-url.js.map +1 -0
  224. package/dist/types.d.ts +59 -0
  225. package/dist/types.d.ts.map +1 -0
  226. package/dist/types.js +17 -0
  227. package/dist/types.js.map +1 -0
  228. package/package.json +84 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Qbtrix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ <!-- packages/core/README.md — @ripple-ui/core. Created 2026-08-25 (monorepo split). -->
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @ripple-ui/core
4
+
5
+ Ripple's spec engine, with no renderer attached.
6
+
7
+ An LLM produces a declarative JSON spec; this package turns it into a
8
+ resolved tree of plain objects — expressions evaluated, conditions
9
+ decided, loops expanded. What you draw that tree with is your business.
10
+
11
+ ```bash
12
+ npm install @ripple-ui/core
13
+ ```
14
+
15
+ ```ts
16
+ import { createHeadlessRuntime } from '@ripple-ui/core';
17
+
18
+ const rt = createHeadlessRuntime({
19
+ spec: {
20
+ type: 'container',
21
+ children: [
22
+ { type: 'text', id: 'label', props: { text: 'Count: {state.count}' } },
23
+ {
24
+ type: 'button',
25
+ id: 'inc',
26
+ on_click: { action: 'set', target: 'count', value: '{state.count + 1}' }
27
+ }
28
+ ]
29
+ },
30
+ state: { count: 0 }
31
+ });
32
+
33
+ rt.findById('label').props.text; // 'Count: 0'
34
+ await rt.dispatch(rt.findById('inc'), 'onclick');
35
+ rt.findById('label').props.text; // 'Count: 1'
36
+ ```
37
+
38
+ ## What's in here
39
+
40
+ - **Schema** — the `UISpec` / `UniversalSpec` zod schemas, event handlers,
41
+ motion, and brand tokens.
42
+ - **Expressions** — `{state.foo}`, `{item.price}`, ternaries, comparisons,
43
+ arithmetic, and template strings.
44
+ - **State** — `StateStore`, the interface every Ripple runtime implements,
45
+ and `HeadlessStateManager`, the dependency-free implementation.
46
+ - **Events** — the dispatcher: 21 action verbs, multi-step flows, async
47
+ chaining, host delegation.
48
+ - **Motion** — the animation compiler and presets (compilation only; playing
49
+ an animation needs a DOM, so that lives in a renderer).
50
+ - **Headless runtime** — spec + state in, `ResolvedNode` tree out.
51
+
52
+ No widgets. A resolved tree names widget *types*; how a `table` draws is a
53
+ renderer's job.
54
+
55
+ ## Framework-agnostic, enforced
56
+
57
+ Nothing reachable from this package imports Svelte, React, or any other
58
+ framework, and nothing touches `document` at import time. That is checked
59
+ rather than promised: `headless/purity.test.ts` crawls the transitive import
60
+ graph and fails the build on any framework import or top-level DOM access.
61
+
62
+ Practically: this runs in Node, a Worker, a test file, or under any UI
63
+ framework.
64
+
65
+ ## Renderers
66
+
67
+ | Package | Renders with |
68
+ |---|---|
69
+ | [`@ripple-ui/svelte`](../svelte) | Svelte 5 |
70
+
71
+ Writing another one means implementing a function over `ResolvedNode` —
72
+ every node carries its resolved props, its bound state path plus the
73
+ prop/event pair to wire it to, and its raw handler specs.
74
+
75
+ ## Docs
76
+
77
+ Full documentation: https://ripple.pocketpaw.xyz/docs. The headless runtime specifically:
78
+ https://ripple.pocketpaw.xyz/docs/concepts/headless. API: https://ripple.pocketpaw.xyz/docs/api/core.
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,93 @@
1
+ # Third-party notices
2
+
3
+ Ripple is MIT licensed (see `LICENSE`). It includes code from the projects
4
+ below. Each section names the files that carry that code and reproduces the
5
+ upstream licence in full, as the licence requires.
6
+
7
+ Other credits kept next to what they cover:
8
+
9
+ - `packages/svelte/NOTICE`: beautiful-ui and the other MIT design sources
10
+ behind the Svelte component skin.
11
+ - `packages/svelte/static/models/CREDITS.md`: author, licence and source for
12
+ every 3D model in `static/models` (CC0, public domain, one CC BY 4.0).
13
+ - `packages/svelte/static/fonts/OFL-*.txt`: the SIL Open Font License for each
14
+ bundled font.
15
+
16
+ ## Anatomy
17
+
18
+ - Source: https://github.com/wheresryan22/anatomy (commit 689dcf9)
19
+ - Project page: https://skills.wheresryan.sh/anatomy
20
+ - Copyright (c) 2026 Ryan, MIT License (full text below)
21
+
22
+ Files derived from it:
23
+
24
+ - `packages/core/src/iso/kit.ts`: the camera, projection, extruded solids,
25
+ outlines and SVG helpers are ported from `kit/iso-kit.ts`; the `ISO_CSS`
26
+ selector structure is ported from `kit/iso.css` (the colour tokens are
27
+ Ripple's own).
28
+ - `packages/core/src/iso/solids.ts`: frame, light, band, hull, prism and
29
+ sphere are ported from `kit/lathe.mjs` (cone and gear ring are new).
30
+ - `packages/core/src/iso/scene.ts` and `packages/core/src/iso/enrich.ts` are
31
+ new code built on the two files above; `enrich.ts` follows Anatomy's craft
32
+ rules for detail.
33
+ - `packages/svelte/src/lib/figures/*` and
34
+ `packages/svelte/src/lib/widgets/composite/IsoScene.svelte` draw with the
35
+ kit above.
36
+
37
+ ```
38
+ MIT License
39
+
40
+ Copyright (c) 2026 Ryan
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining a copy
43
+ of this software and associated documentation files (the "Software"), to deal
44
+ in the Software without restriction, including without limitation the rights
45
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46
+ copies of the Software, and to permit persons to whom the Software is
47
+ furnished to do so, subject to the following conditions:
48
+
49
+ The above copyright notice and this permission notice shall be included in all
50
+ copies or substantial portions of the Software.
51
+
52
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
58
+ SOFTWARE.
59
+ ```
60
+
61
+ ## ZzFX
62
+
63
+ - Source: https://github.com/KilledByAPixel/ZzFX (v1.4.0)
64
+ - Copyright (c) 2019 - Frank Force, MIT License (full text below)
65
+
66
+ Files derived from it:
67
+
68
+ - `packages/svelte/src/lib/sound/zzfx.ts`: the sample builder, vendored and
69
+ typed.
70
+
71
+ ```
72
+ ZzFX MIT License
73
+
74
+ Copyright (c) 2019 - Frank Force
75
+
76
+ Permission is hereby granted, free of charge, to any person obtaining a copy
77
+ of this software and associated documentation files (the "Software"), to deal
78
+ in the Software without restriction, including without limitation the rights
79
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
80
+ copies of the Software, and to permit persons to whom the Software is
81
+ furnished to do so, subject to the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be included in all
84
+ copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
87
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
88
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
89
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
90
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
91
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
92
+ SOFTWARE.
93
+ ```
@@ -0,0 +1,148 @@
1
+ import type { AuditItem, AuditReport } from '../core/audit-report.js';
2
+ import { type DataGuardResult, type Fact } from '../core/data-guard.js';
3
+ import type { ValidateCatalogOptions } from '../core/validate-catalog.js';
4
+ import type { SlimWidgetEntry } from '../manifest/slim.js';
5
+ import { type CraftRule, type Lens, type Waiver } from '../manifest/craft.js';
6
+ export type Preset = 'card' | 'figure';
7
+ export type StopReason = 'clean' | 'cap' | 'guard' | 'no-progress';
8
+ /** What the loop asks the model for on each round. The host turns it into a prompt. */
9
+ export interface ModelRequest {
10
+ preset: Preset;
11
+ /** The host's ask and rules, the same every round. */
12
+ prompt: string;
13
+ /** 1-based round number. */
14
+ round: number;
15
+ /** The best spec so far, for the model to fix; absent on round 1 or until a round parses. */
16
+ prior?: unknown;
17
+ /** The faults of the last round as one text block; `''` on round 1. */
18
+ feedback: string;
19
+ signal?: AbortSignal;
20
+ }
21
+ /** A model call. `tokens` is what the call cost; a host whose provider reports none counts before returning. */
22
+ export type Model = (req: ModelRequest) => Promise<{
23
+ text: string;
24
+ tokens: number;
25
+ }>;
26
+ /** An offender's box on a render (CSS px from the shot's top left), for the critic to point at. */
27
+ export interface Box {
28
+ /** The page check that found it, e.g. `overflow`. */
29
+ check: string;
30
+ /** JSON Pointer of the spec node it belongs to, when the host could map it. */
31
+ path?: string;
32
+ x: number;
33
+ y: number;
34
+ w: number;
35
+ h: number;
36
+ }
37
+ /** One render of the spec at one width and theme. */
38
+ export interface RenderResult {
39
+ width: number;
40
+ theme: string;
41
+ /** Where the host put the screenshot (a path or URL), or null when nothing was shot. */
42
+ png: string | null;
43
+ /** The render checks for this shot; every item counts as a fault. */
44
+ audit: AuditReport<AuditItem>[];
45
+ boxes: Box[];
46
+ }
47
+ export type Renderer = (spec: unknown) => Promise<RenderResult[]>;
48
+ /** A finding from the host's critic (the eyes on the screenshots). */
49
+ export interface CriticIssue extends AuditItem {
50
+ /** A `craft.lenses` id. */
51
+ lens: string;
52
+ /** The `craft.rules` id it breaks, when it names one. */
53
+ rule?: string;
54
+ /** The widget type at `path`, filled by the loop when it lands in `widgetFaults`. */
55
+ widget?: string;
56
+ severity: 1 | 2 | 3;
57
+ /** `spec`: the model can fix it. `widget`: the widget is at fault, reported, not scored. */
58
+ fixable: 'spec' | 'widget';
59
+ }
60
+ /** The craft data a critic judges by: rules with ids, and the lenses it looks through. */
61
+ export interface CriticRules {
62
+ rules: readonly CraftRule[];
63
+ lenses: readonly Lens[];
64
+ }
65
+ export type Critic = (req: {
66
+ spec: unknown;
67
+ renders: RenderResult[];
68
+ rules: CriticRules;
69
+ prompt: string;
70
+ signal?: AbortSignal;
71
+ }) => Promise<{
72
+ issues: CriticIssue[];
73
+ tokens: number;
74
+ }>;
75
+ export interface BuildLimits {
76
+ /** Round cap. Default 4. */
77
+ rounds?: number;
78
+ /** Model plus critic tokens per build. Default 200,000. */
79
+ tokens?: number;
80
+ }
81
+ export interface BuildOptions extends ValidateCatalogOptions {
82
+ preset: Preset;
83
+ prompt: string;
84
+ model: Model;
85
+ renderer: Renderer;
86
+ critic?: Critic;
87
+ /** Slim widget entries for prop checks, the guard and the pre-gate. */
88
+ widgets?: readonly SlimWidgetEntry[];
89
+ /** The user's facts; the data guard's baseline when any is `given`, else the first parsed round. */
90
+ facts?: readonly Fact[];
91
+ /** Reviewed fault exceptions. Default: craft's `waivers`. */
92
+ waivers?: readonly Waiver[];
93
+ /** What the critic judges by. Default: core's `craft`. */
94
+ craft?: CriticRules;
95
+ /** The caller's clock: an ISO timestamp and an IANA zone. */
96
+ now: {
97
+ iso: string;
98
+ tz: string;
99
+ };
100
+ limits?: BuildLimits;
101
+ signal?: AbortSignal;
102
+ onRound?: (round: Round) => void;
103
+ }
104
+ export interface Round {
105
+ n: number;
106
+ /** The parsed spec, or null when the model's text was not JSON. */
107
+ spec: unknown;
108
+ /** Lossless fixes applied before the audit, in words. */
109
+ fixed: string[];
110
+ /** Pure audit, plus a `parse` report when the text did not parse. */
111
+ audit: AuditReport<AuditItem>[];
112
+ renders: RenderResult[];
113
+ critic: CriticIssue[];
114
+ /** Pure and render audit items left after waivers. */
115
+ faults: number;
116
+ /** What the `spec` critic issues cost (4, 8 or 12 each by severity). */
117
+ penalty: number;
118
+ score: number;
119
+ accepted: boolean;
120
+ /** True when round 1 took the pre-gate: one critic look, then stop. */
121
+ preGate?: boolean;
122
+ /** Items a waiver took out of the count. */
123
+ waived: AuditItem[];
124
+ /** The data guard's verdict; absent when the round did not parse. */
125
+ guard?: DataGuardResult;
126
+ /** Tokens this round's model and critic calls cost. */
127
+ tokens: number;
128
+ /** The feedback this round produced for the next one. */
129
+ feedback: string;
130
+ }
131
+ export interface BuildResult {
132
+ /** The best accepted spec, or null when no round parsed. */
133
+ spec: unknown;
134
+ rounds: Round[];
135
+ /** Faults no spec can fix: the best round's `widget` critic issues and any guard conflict. */
136
+ widgetFaults: CriticIssue[];
137
+ stopped: StopReason;
138
+ tokens: number;
139
+ }
140
+ /** The model's text as a spec: a fenced block's body, or the outermost `{...}`; trailing commas forgiven. */
141
+ export declare function parseSpecText(text: string): {
142
+ spec: unknown;
143
+ fixed?: string;
144
+ } | {
145
+ error: string;
146
+ };
147
+ export declare function buildSpec(opts: BuildOptions): Promise<BuildResult>;
148
+ //# sourceMappingURL=build-spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-spec.d.ts","sourceRoot":"","sources":["../../src/build/build-spec.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,EAAuB,KAAK,eAAe,EAAE,KAAK,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAG7F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAA2D,KAAK,SAAS,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGvI,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AACvC,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,GAAG,aAAa,CAAC;AAEnE,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,6FAA6F;IAC7F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,gHAAgH;AAChH,MAAM,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAErF,mGAAmG;AACnG,MAAM,WAAW,GAAG;IACnB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACV;AAED,qDAAqD;AACrD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;IAChC,KAAK,EAAE,GAAG,EAAE,CAAC;CACb;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;AAElE,sEAAsE;AACtE,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC7C,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,4FAA4F;IAC5F,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC3B;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IAC5B,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC;CACxB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC3B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAa,SAAQ,sBAAsB;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACrC,oGAAoG;IACpG,KAAK,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IACxB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0DAA0D;IAC1D,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,6DAA6D;IAC7D,GAAG,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,KAAK;IACrB,CAAC,EAAE,MAAM,CAAC;IACV,mEAAmE;IACnE,IAAI,EAAE,OAAO,CAAC;IACd,yDAAyD;IACzD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;IAChC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,qEAAqE;IACrE,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC3B,4DAA4D;IAC5D,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,8FAA8F;IAC9F,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,OAAO,EAAE,UAAU,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CACf;AAwBD,6GAA6G;AAC7G,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAiBjG;AAiED,wBAAsB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAgJxE"}
@@ -0,0 +1,297 @@
1
+ // @file build/build-spec.ts
2
+ // @description The build loop: a model writes a spec in rounds, the pure audit,
3
+ // the data guard, the host's renderer and the host's critic check it, and
4
+ // every fault goes back to the model as one feedback block until a round is
5
+ // clean, stops improving, or a cap is hit. The model, renderer and critic are
6
+ // injected functions; this module holds no provider, no keys and no I/O.
7
+ // Invariants:
8
+ // - Pure. No clock, no files, no DOM. Time comes in as `now`; the wall-clock
9
+ // cap comes in as the caller's `signal`. An abort stops the loop with
10
+ // `stopped: 'cap'` and the best round so far.
11
+ // - Each round: model, parse (one trailing-comma retry), lossless fixes (an
12
+ // envelope alias moved to `ui`), pure audit, data guard, renderer, critic.
13
+ // - Score is 100 - 10 x faults - 2 x waived - critic penalty. Faults are the
14
+ // pure and render audit items left after waivers; each critic issue marked
15
+ // `fixable: 'spec'` costs 4, 8 or 12 by severity. `widget` issues cost
16
+ // nothing and go to `widgetFaults`. An item a live waiver covers (same
17
+ // check, on the waived widget, named by the item or found through its path) moves to its
18
+ // report's `waived` and to `Round.waived`: out of the count, still recorded.
19
+ // - A round is accepted only when guard-clean and strictly better than the
20
+ // best, so the returned spec is never worse than the first accepted round.
21
+ // A round that does not parse is recorded but never returned. Clean = no
22
+ // faults and no `spec` critic issue on a round the critic looked at.
23
+ // - The data guard runs after the pure audit on every parsed round, against
24
+ // the host's facts or the first parsed round. A round that fails it is
25
+ // never accepted and never rendered: the loop stops `guard`, keeps the
26
+ // best guard-clean round (null if there is none) and moves the conflict
27
+ // to `widgetFaults`. There is no second bend.
28
+ // - Token cap (`limits.tokens`, default 200k) is summed after every model
29
+ // and critic call. A model call that crosses it leaves its round unrendered
30
+ // and unaccepted; a critic call that crosses it ends the loop after that
31
+ // round is scored (`clean` if it is, else `cap`). The best accepted round
32
+ // is kept.
33
+ // - Pre-gate: a round-1 spec that is one data widget (in single-child
34
+ // wrappers) with clean audits gets one critic look and the loop stops:
35
+ // `clean` when the critic is quiet, else `no-progress`.
36
+ import { dataGuard, dayAfter } from '../core/data-guard.js';
37
+ import { specAudit } from '../core/spec-audit.js';
38
+ import { formatSpecIssues } from '../core/spec-issues.js';
39
+ import { craft as craftData, DATA_PROPS, waivers as craftWaivers } from '../manifest/craft.js';
40
+ import { specEnvelope } from '../manifest/envelope.js';
41
+ const DEFAULT_ROUNDS = 4;
42
+ const DEFAULT_TOKENS = 200_000;
43
+ /** Drop each comma that only a `}` or `]` follows, outside strings: the one JSON slip models make that loses nothing. */
44
+ function dropTrailingCommas(text) {
45
+ let out = '';
46
+ let inString = false;
47
+ for (let i = 0; i < text.length; i++) {
48
+ const c = text[i];
49
+ if (inString) {
50
+ if (c === '\\') {
51
+ out += c + (text[++i] ?? '');
52
+ continue;
53
+ }
54
+ if (c === '"')
55
+ inString = false;
56
+ }
57
+ else if (c === '"')
58
+ inString = true;
59
+ else if (c === ',' && /^\s*[}\]]/.test(text.slice(i + 1)))
60
+ continue;
61
+ out += c;
62
+ }
63
+ return out;
64
+ }
65
+ /** The model's text as a spec: a fenced block's body, or the outermost `{...}`; trailing commas forgiven. */
66
+ export function parseSpecText(text) {
67
+ const fenced = /```(?:json)?\s*\n([\s\S]*?)```/.exec(text);
68
+ let body = fenced ? fenced[1] : text;
69
+ if (!fenced) {
70
+ const start = body.indexOf('{');
71
+ const end = body.lastIndexOf('}');
72
+ if (start >= 0 && end > start)
73
+ body = body.slice(start, end + 1);
74
+ }
75
+ try {
76
+ return { spec: JSON.parse(body) };
77
+ }
78
+ catch (e) {
79
+ try {
80
+ return { spec: JSON.parse(dropTrailingCommas(body)), fixed: 'dropped trailing commas' };
81
+ }
82
+ catch {
83
+ return { error: `the reply is not one JSON spec (${e.message})` };
84
+ }
85
+ }
86
+ }
87
+ /** Move a lone envelope alias (`root`, `tree`, ...) to `ui` when `ui` is missing. Mutates; returns what it did. */
88
+ function fixEnvelope(spec) {
89
+ if (typeof spec !== 'object' || spec === null || Array.isArray(spec))
90
+ return [];
91
+ const s = spec;
92
+ const found = specEnvelope.aliasesNotAllowed.filter((a) => typeof s[a] === 'object' && s[a] !== null);
93
+ if ('ui' in s || found.length !== 1)
94
+ return [];
95
+ s.ui = s[found[0]];
96
+ delete s[found[0]];
97
+ return [`moved \`${found[0]}\` to \`ui\``];
98
+ }
99
+ class Aborted extends Error {
100
+ }
101
+ /** The widget type of the spec node an item's path points into (`/ui/children/0/props/x` → that child's type). */
102
+ function widgetAt(spec, path) {
103
+ const node = /^\/ui((?:\/(?:children|else_children)\/\d+)*)/.exec(path);
104
+ if (!node || typeof spec !== 'object' || spec === null)
105
+ return undefined;
106
+ let at = spec.ui;
107
+ for (const seg of node[1].split('/').slice(1))
108
+ at = at?.[seg];
109
+ const type = at?.type;
110
+ return typeof type === 'string' ? type : undefined;
111
+ }
112
+ /** True when the spec's tree is one data widget, alone or inside single-child wrappers. */
113
+ function oneDataWidget(spec, widgets) {
114
+ let node = spec?.ui;
115
+ while (node && Array.isArray(node.children) && node.children.length === 1)
116
+ node = node.children[0];
117
+ const type = node?.type;
118
+ if (typeof type !== 'string' || (Array.isArray(node?.children) && node.children.length > 0))
119
+ return false;
120
+ const entry = widgets?.find((w) => w.type === type);
121
+ return type in DATA_PROPS || Object.values(entry?.props ?? {}).some((p) => p?.data === true);
122
+ }
123
+ /**
124
+ * Move each item a live waiver covers out of its report's count into
125
+ * `waived`; returns the moved items. Matches by the item's `widget` (a
126
+ * render item names the widget it sits in), else the node its path points to.
127
+ */
128
+ function waive(reports, spec, list, day) {
129
+ const live = list.filter((w) => !w.until || w.until >= day);
130
+ const moved = [];
131
+ for (const r of reports) {
132
+ const keep = [];
133
+ for (const item of r.items) {
134
+ const widget = live.some((w) => w.check === r.check) ? (item.widget ?? widgetAt(spec, item.path)) : undefined;
135
+ if (widget && live.some((w) => w.check === r.check && w.widget === widget))
136
+ moved.push(item);
137
+ else
138
+ keep.push(item);
139
+ }
140
+ if (keep.length === r.items.length)
141
+ continue;
142
+ r.waived = [...(r.waived ?? []), ...r.items.filter((i) => !keep.includes(i))];
143
+ r.items = keep;
144
+ r.count = keep.length;
145
+ }
146
+ return moved;
147
+ }
148
+ /** A critic issue as a feedback line: its lens and rule up front so the model can find the rule. */
149
+ const criticLine = (i) => ({
150
+ code: i.rule ?? i.code,
151
+ path: i.path,
152
+ message: `[${i.lens}${i.rule ? `, rule:${i.rule}` : ''}] ${i.message}`
153
+ });
154
+ export async function buildSpec(opts) {
155
+ const { preset, prompt, model, renderer, critic, signal, onRound } = opts;
156
+ const list = opts.waivers ?? craftWaivers;
157
+ const rules = opts.craft ?? craftData;
158
+ const day = dayAfter(opts.now).today; // in the caller's zone, as the guard reads it
159
+ let first = undefined; // the first parsed spec, the guard's baseline without facts
160
+ const cap = opts.limits?.rounds ?? DEFAULT_ROUNDS;
161
+ const tokenCap = opts.limits?.tokens ?? DEFAULT_TOKENS;
162
+ const rounds = [];
163
+ let best = null;
164
+ let widgetFaults = [];
165
+ let tokens = 0;
166
+ let stopped = 'cap';
167
+ const check = () => {
168
+ if (signal?.aborted)
169
+ throw new Aborted();
170
+ };
171
+ /** Run a host call; an abort during it ends the loop (even if the call returns), any other error is the host's. */
172
+ const guarded = async (call) => {
173
+ check();
174
+ let r;
175
+ try {
176
+ r = await call();
177
+ }
178
+ catch (e) {
179
+ if (signal?.aborted)
180
+ throw new Aborted();
181
+ throw e;
182
+ }
183
+ check();
184
+ return r;
185
+ };
186
+ try {
187
+ for (let n = 1; n <= cap; n++) {
188
+ const last = rounds.at(-1);
189
+ const reply = await guarded(() => model({ preset, prompt, round: n, prior: best?.spec, feedback: last?.feedback ?? '', signal }));
190
+ tokens += reply.tokens;
191
+ const round = {
192
+ n,
193
+ spec: null,
194
+ fixed: [],
195
+ audit: [],
196
+ renders: [],
197
+ critic: [],
198
+ faults: 0,
199
+ penalty: 0,
200
+ score: 0,
201
+ accepted: false,
202
+ waived: [],
203
+ tokens: reply.tokens,
204
+ feedback: ''
205
+ };
206
+ const parsed = parseSpecText(reply.text);
207
+ if ('error' in parsed) {
208
+ round.audit = [{ check: 'parse', count: 1, items: [{ code: 'parse', path: '', message: parsed.error }] }];
209
+ }
210
+ else {
211
+ round.spec = parsed.spec;
212
+ round.fixed = [...(parsed.fixed ? [parsed.fixed] : []), ...fixEnvelope(parsed.spec)];
213
+ round.audit = specAudit(parsed.spec, opts);
214
+ round.waived = waive(round.audit, parsed.spec, list, day);
215
+ if (first === undefined)
216
+ first = parsed.spec;
217
+ round.guard = dataGuard({ before: first, after: parsed.spec, facts: opts.facts, now: opts.now, widgets: opts.widgets });
218
+ }
219
+ if (round.guard && !round.guard.ok) {
220
+ const items = round.audit.flatMap((r) => r.items);
221
+ round.faults = items.length;
222
+ round.score = 100 - 10 * round.faults - 2 * round.waived.length;
223
+ round.feedback = formatSpecIssues([...round.guard.items, ...items]);
224
+ rounds.push(round);
225
+ widgetFaults = [
226
+ ...widgetFaults,
227
+ ...round.guard.items.map((i) => ({
228
+ ...i,
229
+ lens: 'data-truth',
230
+ rule: i.code === 'inventedDate' ? 'no-future-dates' : 'data-fixed',
231
+ severity: 3,
232
+ fixable: 'widget'
233
+ }))
234
+ ];
235
+ onRound?.(round);
236
+ stopped = 'guard';
237
+ break;
238
+ }
239
+ // Over the token cap after the model call: no render or critic spend, so the round is never accepted.
240
+ const looked = round.spec !== null && tokens < tokenCap;
241
+ if (looked) {
242
+ const spec = round.spec;
243
+ round.renders = await guarded(() => renderer(spec));
244
+ for (const r of round.renders)
245
+ round.waived.push(...waive(r.audit, spec, list, day));
246
+ if (critic) {
247
+ const seen = await guarded(() => critic({ spec, renders: round.renders, rules, prompt, signal }));
248
+ tokens += seen.tokens;
249
+ round.tokens += seen.tokens;
250
+ round.critic = seen.issues.map((i) => (i.fixable === 'widget' ? { ...i, widget: i.widget ?? widgetAt(spec, i.path) } : i));
251
+ }
252
+ }
253
+ const items = [
254
+ ...round.audit.flatMap((r) => r.items),
255
+ ...round.renders.flatMap((r) => r.audit.flatMap((a) => a.items.map((i) => ({ ...i, message: `[${r.width}-${r.theme}] ${i.message}` }))))
256
+ ];
257
+ const specIssues = round.critic.filter((i) => i.fixable === 'spec');
258
+ round.faults = items.length;
259
+ round.penalty = specIssues.reduce((sum, i) => sum + 4 * i.severity, 0);
260
+ round.score = 100 - 10 * round.faults - 2 * round.waived.length - round.penalty;
261
+ round.feedback = formatSpecIssues([...items, ...specIssues.map(criticLine)]);
262
+ round.accepted = looked && (!best || round.score > best.score);
263
+ const isClean = looked && round.faults === 0 && specIssues.length === 0;
264
+ if (n === 1 && looked && round.faults === 0 && oneDataWidget(round.spec, opts.widgets))
265
+ round.preGate = true;
266
+ rounds.push(round);
267
+ if (round.accepted) {
268
+ best = round;
269
+ widgetFaults = round.critic.filter((i) => i.fixable === 'widget');
270
+ }
271
+ onRound?.(round);
272
+ if (round.accepted && isClean) {
273
+ stopped = 'clean';
274
+ break;
275
+ }
276
+ if (tokens >= tokenCap) {
277
+ stopped = 'cap';
278
+ break;
279
+ }
280
+ if (round.preGate) {
281
+ stopped = 'no-progress';
282
+ break;
283
+ }
284
+ if (best && round.spec !== null && !round.accepted) {
285
+ stopped = 'no-progress';
286
+ break;
287
+ }
288
+ }
289
+ }
290
+ catch (e) {
291
+ if (!(e instanceof Aborted))
292
+ throw e;
293
+ stopped = 'cap';
294
+ }
295
+ return { spec: best?.spec ?? null, rounds, widgetFaults, stopped, tokens };
296
+ }
297
+ //# sourceMappingURL=build-spec.js.map