@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.
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/THIRD_PARTY_NOTICES.md +93 -0
- package/dist/build/build-spec.d.ts +148 -0
- package/dist/build/build-spec.d.ts.map +1 -0
- package/dist/build/build-spec.js +297 -0
- package/dist/build/build-spec.js.map +1 -0
- package/dist/build/index.d.ts +6 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +7 -0
- package/dist/build/index.js.map +1 -0
- package/dist/core/audit-report.d.ts +24 -0
- package/dist/core/audit-report.d.ts.map +1 -0
- package/dist/core/audit-report.js +12 -0
- package/dist/core/audit-report.js.map +1 -0
- package/dist/core/base-dispatcher.d.ts +115 -0
- package/dist/core/base-dispatcher.d.ts.map +1 -0
- package/dist/core/base-dispatcher.js +311 -0
- package/dist/core/base-dispatcher.js.map +1 -0
- package/dist/core/brand-applier.d.ts +39 -0
- package/dist/core/brand-applier.d.ts.map +1 -0
- package/dist/core/brand-applier.js +179 -0
- package/dist/core/brand-applier.js.map +1 -0
- package/dist/core/data-guard.d.ts +36 -0
- package/dist/core/data-guard.d.ts.map +1 -0
- package/dist/core/data-guard.js +232 -0
- package/dist/core/data-guard.js.map +1 -0
- package/dist/core/event-dispatcher.d.ts +218 -0
- package/dist/core/event-dispatcher.d.ts.map +1 -0
- package/dist/core/event-dispatcher.js +502 -0
- package/dist/core/event-dispatcher.js.map +1 -0
- package/dist/core/expression-resolver.d.ts +122 -0
- package/dist/core/expression-resolver.d.ts.map +1 -0
- package/dist/core/expression-resolver.js +1137 -0
- package/dist/core/expression-resolver.js.map +1 -0
- package/dist/core/flow-spec.d.ts +46 -0
- package/dist/core/flow-spec.d.ts.map +1 -0
- package/dist/core/flow-spec.js +69 -0
- package/dist/core/flow-spec.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/normalizer.d.ts +16 -0
- package/dist/core/normalizer.d.ts.map +1 -0
- package/dist/core/normalizer.js +53 -0
- package/dist/core/normalizer.js.map +1 -0
- package/dist/core/spec-audit.d.ts +25 -0
- package/dist/core/spec-audit.d.ts.map +1 -0
- package/dist/core/spec-audit.js +389 -0
- package/dist/core/spec-audit.js.map +1 -0
- package/dist/core/spec-id.d.ts +27 -0
- package/dist/core/spec-id.d.ts.map +1 -0
- package/dist/core/spec-id.js +74 -0
- package/dist/core/spec-id.js.map +1 -0
- package/dist/core/spec-issues.d.ts +28 -0
- package/dist/core/spec-issues.d.ts.map +1 -0
- package/dist/core/spec-issues.js +62 -0
- package/dist/core/spec-issues.js.map +1 -0
- package/dist/core/spec-mutator.d.ts +121 -0
- package/dist/core/spec-mutator.d.ts.map +1 -0
- package/dist/core/spec-mutator.js +394 -0
- package/dist/core/spec-mutator.js.map +1 -0
- package/dist/core/state-mutator.d.ts +37 -0
- package/dist/core/state-mutator.d.ts.map +1 -0
- package/dist/core/state-mutator.js +215 -0
- package/dist/core/state-mutator.js.map +1 -0
- package/dist/core/state-store.d.ts +45 -0
- package/dist/core/state-store.d.ts.map +1 -0
- package/dist/core/state-store.js +25 -0
- package/dist/core/state-store.js.map +1 -0
- package/dist/core/text-coerce.d.ts +17 -0
- package/dist/core/text-coerce.d.ts.map +1 -0
- package/dist/core/text-coerce.js +61 -0
- package/dist/core/text-coerce.js.map +1 -0
- package/dist/core/theme-applier.d.ts +6 -0
- package/dist/core/theme-applier.d.ts.map +1 -0
- package/dist/core/theme-applier.js +63 -0
- package/dist/core/theme-applier.js.map +1 -0
- package/dist/core/validate-catalog.d.ts +35 -0
- package/dist/core/validate-catalog.d.ts.map +1 -0
- package/dist/core/validate-catalog.js +67 -0
- package/dist/core/validate-catalog.js.map +1 -0
- package/dist/core/widget-bind-contract.d.ts +36 -0
- package/dist/core/widget-bind-contract.d.ts.map +1 -0
- package/dist/core/widget-bind-contract.js +204 -0
- package/dist/core/widget-bind-contract.js.map +1 -0
- package/dist/core/widget-registry.d.ts +54 -0
- package/dist/core/widget-registry.d.ts.map +1 -0
- package/dist/core/widget-registry.js +88 -0
- package/dist/core/widget-registry.js.map +1 -0
- package/dist/headless/index.d.ts +34 -0
- package/dist/headless/index.d.ts.map +1 -0
- package/dist/headless/index.js +30 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/headless/resolve-tree.d.ts +46 -0
- package/dist/headless/resolve-tree.d.ts.map +1 -0
- package/dist/headless/resolve-tree.js +232 -0
- package/dist/headless/resolve-tree.js.map +1 -0
- package/dist/headless/runtime-base.d.ts +94 -0
- package/dist/headless/runtime-base.d.ts.map +1 -0
- package/dist/headless/runtime-base.js +141 -0
- package/dist/headless/runtime-base.js.map +1 -0
- package/dist/headless/runtime.d.ts +49 -0
- package/dist/headless/runtime.d.ts.map +1 -0
- package/dist/headless/runtime.js +52 -0
- package/dist/headless/runtime.js.map +1 -0
- package/dist/headless/slim.d.ts +41 -0
- package/dist/headless/slim.d.ts.map +1 -0
- package/dist/headless/slim.js +41 -0
- package/dist/headless/slim.js.map +1 -0
- package/dist/headless/state.d.ts +36 -0
- package/dist/headless/state.d.ts.map +1 -0
- package/dist/headless/state.js +118 -0
- package/dist/headless/state.js.map +1 -0
- package/dist/headless/types.d.ts +95 -0
- package/dist/headless/types.d.ts.map +1 -0
- package/dist/headless/types.js +22 -0
- package/dist/headless/types.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/iso/audit.d.ts +111 -0
- package/dist/iso/audit.d.ts.map +1 -0
- package/dist/iso/audit.js +319 -0
- package/dist/iso/audit.js.map +1 -0
- package/dist/iso/enrich.d.ts +26 -0
- package/dist/iso/enrich.d.ts.map +1 -0
- package/dist/iso/enrich.js +256 -0
- package/dist/iso/enrich.js.map +1 -0
- package/dist/iso/index.d.ts +12 -0
- package/dist/iso/index.d.ts.map +1 -0
- package/dist/iso/index.js +12 -0
- package/dist/iso/index.js.map +1 -0
- package/dist/iso/kit.d.ts +194 -0
- package/dist/iso/kit.d.ts.map +1 -0
- package/dist/iso/kit.js +685 -0
- package/dist/iso/kit.js.map +1 -0
- package/dist/iso/scene.d.ts +168 -0
- package/dist/iso/scene.d.ts.map +1 -0
- package/dist/iso/scene.js +821 -0
- package/dist/iso/scene.js.map +1 -0
- package/dist/iso/solids.d.ts +49 -0
- package/dist/iso/solids.d.ts.map +1 -0
- package/dist/iso/solids.js +218 -0
- package/dist/iso/solids.js.map +1 -0
- package/dist/manifest/actions.d.ts +42 -0
- package/dist/manifest/actions.d.ts.map +1 -0
- package/dist/manifest/actions.js +303 -0
- package/dist/manifest/actions.js.map +1 -0
- package/dist/manifest/craft.d.ts +51 -0
- package/dist/manifest/craft.d.ts.map +1 -0
- package/dist/manifest/craft.js +458 -0
- package/dist/manifest/craft.js.map +1 -0
- package/dist/manifest/envelope.d.ts +35 -0
- package/dist/manifest/envelope.d.ts.map +1 -0
- package/dist/manifest/envelope.js +41 -0
- package/dist/manifest/envelope.js.map +1 -0
- package/dist/manifest/illustration-svg.d.ts +71 -0
- package/dist/manifest/illustration-svg.d.ts.map +1 -0
- package/dist/manifest/illustration-svg.js +79 -0
- package/dist/manifest/illustration-svg.js.map +1 -0
- package/dist/manifest/index.d.ts +24 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/manifest/index.js +24 -0
- package/dist/manifest/index.js.map +1 -0
- package/dist/manifest/slim-widgets.d.ts +23 -0
- package/dist/manifest/slim-widgets.d.ts.map +1 -0
- package/dist/manifest/slim-widgets.js +95 -0
- package/dist/manifest/slim-widgets.js.map +1 -0
- package/dist/manifest/slim.d.ts +67 -0
- package/dist/manifest/slim.d.ts.map +1 -0
- package/dist/manifest/slim.js +48 -0
- package/dist/manifest/slim.js.map +1 -0
- package/dist/motion/engine.d.ts +19 -0
- package/dist/motion/engine.d.ts.map +1 -0
- package/dist/motion/engine.js +77 -0
- package/dist/motion/engine.js.map +1 -0
- package/dist/motion/load-tier1.d.ts +10 -0
- package/dist/motion/load-tier1.d.ts.map +1 -0
- package/dist/motion/load-tier1.js +24 -0
- package/dist/motion/load-tier1.js.map +1 -0
- package/dist/motion/presets.d.ts +84 -0
- package/dist/motion/presets.d.ts.map +1 -0
- package/dist/motion/presets.js +118 -0
- package/dist/motion/presets.js.map +1 -0
- package/dist/motion/reduce-motion.d.ts +3 -0
- package/dist/motion/reduce-motion.d.ts.map +1 -0
- package/dist/motion/reduce-motion.js +41 -0
- package/dist/motion/reduce-motion.js.map +1 -0
- package/dist/schema/brand.d.ts +185 -0
- package/dist/schema/brand.d.ts.map +1 -0
- package/dist/schema/brand.js +143 -0
- package/dist/schema/brand.js.map +1 -0
- package/dist/schema/event-handler.d.ts +959 -0
- package/dist/schema/event-handler.d.ts.map +1 -0
- package/dist/schema/event-handler.js +309 -0
- package/dist/schema/event-handler.js.map +1 -0
- package/dist/schema/index.d.ts +7 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/motion.d.ts +154 -0
- package/dist/schema/motion.d.ts.map +1 -0
- package/dist/schema/motion.js +25 -0
- package/dist/schema/motion.js.map +1 -0
- package/dist/schema/ui-spec.d.ts +3610 -0
- package/dist/schema/ui-spec.d.ts.map +1 -0
- package/dist/schema/ui-spec.js +225 -0
- package/dist/schema/ui-spec.js.map +1 -0
- package/dist/schema/universal-spec.d.ts +195 -0
- package/dist/schema/universal-spec.d.ts.map +1 -0
- package/dist/schema/universal-spec.js +259 -0
- package/dist/schema/universal-spec.js.map +1 -0
- package/dist/schema/widget-types.d.ts +54 -0
- package/dist/schema/widget-types.d.ts.map +1 -0
- package/dist/schema/widget-types.js +37 -0
- package/dist/schema/widget-types.js.map +1 -0
- package/dist/security/safe-url.d.ts +53 -0
- package/dist/security/safe-url.d.ts.map +1 -0
- package/dist/security/safe-url.js +197 -0
- package/dist/security/safe-url.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- 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
|
-
|
|
1
|
+
<!-- packages/core/README.md — @ripple-ui/core. Created 2026-08-25 (monorepo split). -->
|
|
2
2
|
|
|
3
|
-
|
|
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
|